Day14 · 字典与集合

Day14 · 字典与集合

阶段:阶段一 · 核心基础 | 前置:Day13 | 技术基准:Python 3.15.0

学习目标

  • 掌握字典(dict)的定义、键值对、增删改查
  • 熟练使用 get / setdefault / pop / popitem / update / items
  • 理解字典的 key 为什么必须是可哈希的不可变对象
  • 掌握集合(set)的定义、交并差对称差运算与去重场景
  • 认识 frozenset 与"默认值是可变对象"这个经典陷阱
  • 认识 Python 3.15 新增的内置名字 frozendict(不可变字典)与 sentinel(缺省哨兵)

列表和元组靠"下标"找数据,下标是 0、1、2......这种数字。但现实中我们更常按"名字"查数据:通过用户名查手机号、通过商品编号查价格。这种"按键查值"的容器就是字典(dict),它是开发中使用频率最高的数据结构。

一、字典 dict 是什么

字典是键值对(key-value) 的集合,等价于 Java 的 HashMap,底层是哈希表。定义用花括号 {键: 值}。

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉", "age": 18, "lang": "Python"}
print(info)              # {'name': '小嘉', 'age': 18, 'lang': 'Python'}
print(info["name"])      # 小嘉   通过 key 取 value

三个核心规则:

  1. key 必须唯一且不可变(数字、字符串、元组都行;列表不行,因为它可变、不可哈希);
  2. value 可以是任意类型,随便放;
  3. 从 Python 3.7 开始,字典保留插入顺序(3.6 是 CPython 实现细节,3.7 写进语言规范),但日常仍应"按 key 查"而不是靠顺序。

二、字典的增删改查

2.1、查:get 与直接下标

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉", "age": 18}

print(info["name"])          # 小嘉
# print(info["city"])        # KeyError:key 不存在会崩溃

# 推荐用 get:key 不存在时返回 None 或默认值,不报错
print(info.get("city"))             # None
print(info.get("city", "北京"))     # 北京   不存在就用默认值
print(info.get("name", "未知"))    # 小嘉   存在就返回真实值

小嘉建议:取值优先用 get(),除非你能 100% 确定 key 存在。直接 info["xxx"] 一旦 key 拼错就是 KeyError。

2.2、增与改

同一个写法 d[key] = value:key 不存在就是新增,存在就是修改。

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉"}
info["age"] = 18        # 新增
info["age"] = 19        # 修改(key 已存在)
print(info)             # {'name': '小嘉', 'age': 19}

2.3、setdefault:key 不在就设默认值

setdefault(key, default):key 存在就返回它的值;不存在就先把 key:default 塞进字典再返回 default。统计词频时特别好用:

python 复制代码
# Author: XiaoJia
words = ["a", "b", "a", "c", "a", "b"]
count = {}
for w in words:
    count[w] = count.get(w, 0) + 1     # 写法一:get
print(count)                           # {'a': 3, 'b': 2, 'c': 1}

# 写法二:setdefault
count2 = {}
for w in words:
    count2.setdefault(w, 0)
    count2[w] += 1
print(count2)                          # {'a': 3, 'b': 2, 'c': 1}

2.4、删:pop / popitem / del

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉", "age": 18, "lang": "Python"}

print(info.pop("age"))          # 18   返回被删的值
print(info.pop("city", "没这个key"))   # 没这个key   带默认值不报错
print(info.popitem())           # ('lang', 'Python')  弹出最后一项(元组形式)

# del 直接删,但 key 不存在会 KeyError,不推荐裸用
del info["name"]
print(info)                     # {}

2.5、update:批量合并

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉", "age": 18}
info.update({"age": 19, "city": "北京"})   # 有则改、无则增
info.update(lang="Python")                 # 也支持关键字参数
print(info)        # {'name': '小嘉', 'age': 19, 'city': '北京', 'lang': 'Python'}

三、遍历字典

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉", "age": 18, "lang": "Python"}

# 直接 for 字典,拿到的是 key
for k in info:
    print(k, "=", info[k])

# 推荐:items() 一次拿到 key 和 value,只迭代一次
for k, v in info.items():
    print(f"{k} -> {v}")

# 单独拿所有 key / value
print(list(info.keys()))     # ['name', 'age', 'lang']
print(list(info.values()))   # ['小嘉', 18, 'Python']

用 in 判断 key 是否存在(注意 in 判断的是 key,不是 value):

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉", "age": 18}
print("name" in info)       # True
print("小嘉" in info)        # False   不判断 value

四、字典生成式

和列表生成式类似,只是换成 {k: v for ...}:

python 复制代码
# Author: XiaoJia
# 数字 → 平方
print({i: i * i for i in range(1, 6)})
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# 两个列表按位置拼成字典
keys = ["name", "age", "lang"]
values = ["小嘉", 18, "Python"]
print({k: v for k, v in zip(keys, values)})

# 带条件:只保留 value > 1 的
d = {"a": 1, "b": 2, "c": 3}
print({k: v for k, v in d.items() if v > 1})    # {'b': 2, 'c': 3}

五、默认值是可变对象的陷阱

dict.fromkeys(序列, 默认值) 会用同一个默认值给所有 key 初始化。如果默认值是可变对象(比如列表),所有 key 会共享同一个对象------改一个全变:

python 复制代码
# Author: XiaoJia
keys = ["张三", "李四", "王五"]

# 危险:三个 key 共享同一个列表
bad = dict.fromkeys(keys, [])
bad["张三"].append("数学")
print(bad)     # {'张三': ['数学'], '李四': ['数学'], '王五': ['数学']}  全中招!

# 安全做法:用字典生成式,每个 key 都拿到新列表
good = {k: [] for k in keys}
good["张三"].append("数学")
print(good)    # {'张三': ['数学'], '李四': [], '王五': []}

这个陷阱 Day15 讲深浅拷贝时还会从另一个角度再碰到。

六、集合 set 是什么

集合是无序、不重复 的容器。它最大的两个用途:自动去重 和集合运算 。定义用花括号 {1, 2, 3},注意空集合只能写 set()({} 是空字典)。

python 复制代码
# Author: XiaoJia
s = {1, 2, 3, 2, 1}
print(s)               # {1, 2, 3}   自动去重
print(type(s))         # <class 'set'>

empty = set()          # 空集合必须这样写
print(type({}))        # <class 'dict'>   {} 是空字典,别搞混

6.1、增删元素

python 复制代码
# Author: XiaoJia
s = {1, 2, 3}
s.add(4)               # 加一个
s.update([5, 6])       # 批量加
print(s)               # {1,2,3,4,5,6}

s.remove(1)           # 删除,元素不存在会报 KeyError
s.discard(100)         # 删除不存在的元素,不报错
print(s)               # {2,3,4,5,6}

remove 和 discard 区别:删不存在的元素时,remove 报错、discard 安静跳过。

6.2、集合运算

这是集合的灵魂,对应数学里的集合:

python 复制代码
# Author: XiaoJia
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a | b)      # {1,2,3,4,5,6}   并集
print(a & b)      # {3,4}           交集
print(a - b)      # {1,2}           差集(a 有 b 没有)
print(a ^ b)      # {1,2,5,6}       对称差(只在其中一个里)

# 也有方法写法
print(a.union(b))
print(a.intersection(b))
print({1, 2}.issubset({1, 2, 3}))    # True   a 是 b 的子集

6.3、最常用场景:去重

python 复制代码
# Author: XiaoJia
nums = [1, 2, 2, 3, 3, 3, 1]
unique = list(set(nums))
print(unique)           # [1, 2, 3]

# 注意:set 无序,去重后顺序可能变;需要保序就用别的方法
names = ["a", "b", "a", "c"]
print(list(dict.fromkeys(names)))   # ['a', 'b', 'c']  保序去重技巧

七、frozenset:不可变集合

frozenset 是冻结的、不可变的集合,支持所有集合运算,但不能增删元素。它可哈希,所以能当字典的 key:

python 复制代码
# Author: XiaoJia
fs = frozenset([1, 2, 3, 3])
print(fs)              # frozenset({1, 2, 3})

# 支持集合运算
print(fs & {2, 3, 4})  # frozenset({2, 3})

# fs.add(4)            # AttributeError:不能修改

# 可以当字典 key
d = {fs: "不可变集合"}
print(d)

记忆法:list 之于 tuple,就相当于 set 之于 frozenset。

八、字典为什么快:哈希表原理直觉

列表查一个元素要从头挨个比(最坏 O(n)),而字典按 key 取值几乎是瞬间的(平均 O(1))。秘密在于它底层是哈希表(hash table):存进去时,Python 先用一个哈希函数把 key 算出一个数字(哈希值),映射到内存里的某个位置;取的时候再算一次同样的哈希,直接定位过去。所以字典的查找速度和字典里有 10 项还是 100 万项关系不大,这就是它适合"按键查值"的根本原因。

这也解释了两条铁律:

  • key 必须可哈希:可哈希意味着能稳定算出哈希值。数字、字符串、元组(且内部全不可变)都可哈希;列表可变、每次内容可能变,哈希值就对不上,所以不能当 key。
  • key 不能重复:两个不同的 key 若算到同一个位置叫"哈希冲突",Python 内部会处理,但对外表现就是------后写的同名 key 会覆盖先写的。

九、视图对象与选型建议

9.1、keys/values/items 是动态视图

info.keys() 返回的不是一份普通列表,而是一个动态视图(view) :它永远"实时反映"字典当前的内容。字典改了,视图跟着变;而且它不复制数据,很大的字典也不额外占内存。想得到一份独立的列表就外面包一层 list():

python 复制代码
# Author: XiaoJia
info = {"name": "小嘉"}
keys = info.keys()
print(list(keys))       # ['name']
info["age"] = 18        # 字典变了
print(list(keys))       # ['name', 'age']   视图自动跟着变

9.2、defaultdict 预告

Day14 用 get(k, 0) 或 setdefault 做词频,已经有点啰嗦。标准库 collections.defaultdict 可以更简洁------取不存在的 key 时自动给个默认值,不用每次手动判空,Day27 讲标准库时会专门展开。

9.3、到底用列表还是字典

  • 数据是"一串同类型的东西、主要按顺序处理或遍历" → 列表;
  • 数据是"需要按某个名字/编号快速查" → 字典;
  • 需要"去重、求交并差" → 集合。

工程经验:当你发现自己在写 for x in 列表: if x.id == 目标 这种线性查找时,基本就该改用字典了。

十、3.15 容器新视角:frozendict 与 sentinel

前面的 frozenset 让我们见识了"不可变集合"。Python 3.15 给字典家族也补了一个对应物,同时带来一个专门表示"缺省"的小工具。这两个都是内置名字,不用 import,先混个脸熟即可。

10.1、frozendict:不可变字典(PEP 814)

frozendict 就是"冻结的字典":能读、能查、能当 key,但一旦建好就不能增删改。构造方式和字典几乎一样:

python 复制代码
# Author: XiaoJia
# Python 3.15 新增,frozendict 是内置名字,无需 import
fd = frozendict({"name": "小嘉", "age": 18})
print(fd)                       # frozendict({'name': '小嘉', 'age': 18})
print(frozendict(city="福州"))   # frozendict({'city': '福州'})

# 读和普通字典一模一样
print(fd["name"])               # 小嘉
print(fd.get("age"))            # 18
print(list(fd.items()))         # [('name', '小嘉'), ('age', 18)]

# 想改?不行------它是冻结的
fd["age"] = 19
# TypeError: 'frozendict' object does not support item assignment
del fd["name"]
# TypeError: 'frozendict' object does not support item deletion

它和 dict 有两个容易踩的区别:它不是 dict 的子类,但它可哈希、能当字典 key:

python 复制代码
# Author: XiaoJia
fd = frozendict({"a": 1, "b": 2})

print(isinstance(fd, dict))   # False   注意!它不是 dict 的子类
print(type(fd))               # <class 'frozendict'>

# 可哈希 → 能当字典的 key(普通 dict 可变,反而当不了 key)
cache = {fd: "命中缓存"}
print(cache)                  # {frozendict({'a': 1, 'b': 2}): '命中缓存'}

# 想转回普通字典,外面包一层 dict()
print(dict(fd))               # {'a': 1, 'b': 2}

小嘉提醒:什么时候用它?当你有一份"建好就不该再被改"的键值数据时------比如全局配置项、常量表、发出去之后冻结的请求头。用 frozendict 包一层,任何人想偷偷改都会立刻报错,而不是在运行很久后才发现数据被改乱了。

10.2、sentinel:干净的"缺省"标记(PEP 661)

写函数时常需要一个默认值,表示"用户没传这个参数"。直接用 None 的麻烦在于:没法区分"用户没传"和"用户故意传了 None" 。sentinel("名字") 就是一个专门干这个的唯一标记对象:

python 复制代码
# Author: XiaoJia
# Python 3.15 新增,sentinel 是内置名字
from copy import copy, deepcopy

MISSING = sentinel("MISSING")
print(repr(MISSING))            # MISSING   干净,不带内存地址噪声
print(type(MISSING))            # <class 'sentinel'>

# 复制 / 深复制后仍然是同一个对象
print(copy(MISSING) is MISSING)        # True
print(deepcopy(MISSING) is MISSING)   # True

它的典型用法是当函数默认值,用 is 判断"到底有没有传":

python 复制代码
# Author: XiaoJia
MISSING = sentinel("MISSING")

def greet(name=MISSING):
    if name is MISSING:          # 没传
        return "没传名字"
    return f"你好,{name}"

print(greet())               # 没传名字
print(greet("小嘉"))         # 你好,小嘉

# dict.get 同理:区分"key 不存在"和"key 的值恰好是 None"
info = {"name": None}
print(info.get("name", MISSING) is MISSING)    # False  key 在,值就是 None
print(info.get("city", MISSING) is MISSING)    # True   key 才是真不存在

小嘉提醒:sentinel("MISSING") 每次调用都是一个新对象 ,所以一定要把它存进一个变量(如 MISSING = sentinel("MISSING")),全程用这个变量、用 is 比较,不要现场再造一个。

练习题

  1. 建一个字典存你的姓名、年龄、城市,然后用 get 安全地取出"电话"(不存在时返回"未填写")。
  2. 用字典统计列表 ["apple","banana","apple","orange","banana","apple"] 里每个单词出现的次数。
  3. 有两个列表 [1,2,3,4] 和 [3,4,5,6],用集合求出它们的交集、并集、差集。
  4. 把列表 [1,2,2,3,3,3] 去重,写出至少两种方法。
  5. 解释:为什么 dict.fromkeys(["a","b"], []) 之后给 a 的列表 append 一个元素,b 也跟着变?怎么避免?
  6. 思考题:为什么字典的 key 必须是不可变类型?列表为什么不能当 key?
  7. 用 frozendict 建一份不可变配置 {"host": "localhost", "port": 8080},尝试给它赋值一个新 key,观察报什么错;再把它当 key 放进另一个字典里。

小结

  • 字典 {k: v}:按键查值,key 唯一且不可变;3.7+ 保留插入顺序。
  • 取值优先 get(k, default) 防 KeyError;setdefault 适合词频统计;update 批量合并。
  • 遍历用 items() 一次拿到键值对;in 判断的是 key。
  • 字典生成式 {k: v for ...};警惕 fromkeys 共享可变默认值的陷阱。
  • 集合 set():无序不重复,用途是去重和交并差对称差运算;空集合只能 set()。
  • frozenset 不可变、可哈希,能当字典 key。
  • Python 3.15 新特性:内置名字 frozendict 是不可变字典(不是 dict 子类,但可哈希、能当 key,dict(fd) 转回普通字典);sentinel("MISSING") 当"缺省"标记,配合 is 区分"没传"与"传了 None"。

扩展阅读

相关推荐
cpolar技术支持1 小时前
网关加 gzip,同时开启 chunked,下载回来的包解不开:把两层编码的解压顺序与长度核对做成只读页
python·http·网络编程·socket·内网穿透
字节探索1 小时前
Spring Boot 接口幂等性设计:从唯一索引到 Redis+Lua 的工程实践
java·spring boot
用户3721574261351 小时前
Java 实现 Excel 与 Markdown 相互转换:表格导出、格式设置与多表提取
java
迪菲赫尔曼1 小时前
【基础篇 11】把官方源码放到服务器:版本、解压层级和安装位置,一次弄清楚
运维·服务器·开发语言·人工智能·python·yolo·目标检测
用户8356290780511 小时前
Python 调整 Excel 工作表显示设置
后端·python
SL_staff1 小时前
制造业订单交付总踩坑?用结构化节点协同模型重构计划系统
java·开源·全栈
小李不想当小白1 小时前
SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
chari克里2 小时前
QT5.15.2 + msvc2022部署全流程说明(windows可联网环境)
开发语言·c++·qt
小咕聊编程2 小时前
教育大数据可视化系统
python·django·flask