前言
字典(dict)提供了一批内置方法,名字都短,但行为差别不小:get和setdefault都能「给个默认值」,可一个只读不写、一个会写回去;pop和popitem都删东西,但一个删你指定的键、一个删末尾;keys/values/items返回的是视图而不是列表。这些差别如果只靠猜,很容易写出「看起来对、跑起来错」的代码。
本文不铺陈概念,直接一个方法一个方法地过:给出它的签名(形参怎么传、/前后是位置还是关键字、有没有默认值),再说明它返回什么、是原地修改还是返回新对象。所有签名以 Python 官方文档为准,示例可在 Python 3.8 及以上运行;个别方法标注了更高的版本要求。
逐个方法讲完,最后一节用表格汇总,并列出常见坑点。
一、读值:get(key, default=None, /)
get返回键对应的值;键不存在时返回default,default省略则为None。它从不抛KeyError,也从不修改字典。
# 适用于 Python 3.8+
d = {"a": 1, "b": 2}
print(d.get("a")) # 1
print(d.get("z")) # None
print(d.get("z", 0)) # 0
print(d) # {'a': 1, 'b': 2},字典没变注意签名里的/:d.get("z", 0)这样按位置传没问题,但d.get(key="z")会报TypeError,因为这两个参数都是仅位置参数。
二、缺键才写:setdefault(key, default=None, /)
setdefault是get的「带写入」版本:键在,返回值;键不在,先插入key: default,再返回default。
# 适用于 Python 3.8+
d = {"a": 1}
print(d.setdefault("a", 99)) # 1,已存在就不覆盖
print(d.setdefault("b", 2)) # 2,不存在则写入
print(d) # {'a': 1, 'b': 2}s这个语义让它成为「分组建桶」的利器,后面在实战篇里会用到。它的默认值同样是None。
三、批量更新:update(...)
update有几种调用形式:update(mapping, /, **kwargs)、update(iterable, /, **kwargs)、update(**kwargs)。它原地把另一个映射或键值对序列合并进来,覆盖已有键,返回None。
# 适用于 Python 3.8+
d = {"a": 1}
d.update({"b": 2, "a": 100}) # 传映射
d.update([("c", 3)]) # 传键值对序列
d.update(d=4) # 传关键字参数
print(d) # {'a': 100, 'b': 2, 'c': 3, 'd': 4}update返回None是常见误解点:它不返回新字典。
四、删除:pop(key, /)与popitem()
pop(key, /)删除指定键并返回其值;键不存在且没给默认值就抛KeyError;给了默认值d.pop(key, default)则返回默认值。
# 适用于 Python 3.8+
d = {"a": 1, "b": 2}
print(d.pop("a")) # 1
print(d.pop("z", None)) # None
print(d) # {'b': 2}popitem()不接受参数,删除并返回最后一个键值对,从 Python 3.7 起保证是 LIFO(后进先出)。空字典上调用会抛KeyError。
# 适用于 Python 3.8+
d = {"a": 1, "b": 2, "c": 3}
print(d.popitem()) # ('c', 3),最后插入的先出
print(d.popitem()) # ('b', 2)五、清空与拷贝:clear()和copy()
clear()删除全部项,返回None。copy()返回一个浅拷贝:新字典是新的对象,但里面嵌套的可变值仍与原字典共享。
# 适用于 Python 3.8+
d = {"a": 1, "b": {"n": 1}}
e = d.copy()
e["a"] = 99 # 顶层互不影响
e["b"]["n"] = 88 # ❗ 内层还是同一个对象
print(d) # {'a': 1, 'b': {'n': 88}}
d.clear()
print(d) # {}六、三个视图:keys()、values()、items()
三者都返回视图对象:动态反映字典变化,支持len()和in。keys()的返回项是键,values()是值,items()是(key, value)元组。键视图和项视图还支持集合运算。
# 适用于 Python 3.8+
d = {"a": 1, "b": 2}
print(list(d.keys())) # ['a', 'b']
print(list(d.values())) # [1, 2]
print(list(d.items())) # [('a', 1), ('b', 2)]
print(d.keys() & {"b", "c"}) # {'b'},键视图可做交集有一个反直觉的事实:d.values()之间比较永远为False,即使内容一样,因为值视图不保证唯一、不被当作集合。
# 适用于 Python 3.8+
d = {"a": 1}
print(d.values() == d.values()) # False七、从序列造字典:fromkeys(iterable, value=None, /)
fromkeys是类方法,返回一个新字典,键来自iterable,所有键共用同一个value。
# 适用于 Python 3.8+
keys = ["a", "b", "c"]
d = dict.fromkeys(keys)
print(d) # {'a': None, 'b': None, 'c': None}
d2 = dict.fromkeys(keys, 0)
print(d2) # {'a': 0, 'b': 0, 'c': 0}关键陷阱在下一节第一条:如果value是可变对象(比如空列表),所有键会共享同一个列表。
八、合并:|与|=(Python 3.9+)
d | other返回新字典,d |= other原地更新,两者都是other的值优先。这与集合的|(并集)语义不同,别混。
# 适用于 Python 3.9+
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 30}
print(a | b) # {'x': 1, 'y': 20, 'z': 30},a 未被改动
a |= b
print(a) # {'x': 1, 'y': 20, 'z': 30},a 被改动Python 3.9 之前,等价写法是{**a, **b}(新字典)和a.update(b)(原地)。
常见坑点
坑一:用可变对象做fromkeys的值。
# 适用于 Python 3.8+
# ❌ 三个键共享同一个列表
d = dict.fromkeys(["a", "b", "c"], [])
d["a"].append(1)
print(d) # {'a': [1], 'b': [1], 'c': [1]}
# ✅ 用字典推导式,每个键各建一个
d = {k: [] for k in ["a", "b", "c"]}
d["a"].append(1)
print(d) # {'a': [1], 'b': [], 'c': []}坑二:把setdefault当纯读取用。
# 适用于 Python 3.8+
# ❌ 只想读,却顺手写进了一个键
d = {"a": 1}
d.setdefault("b") # 返回 None,同时 d 变成 {'a': 1, 'b': None}
# ✅ 只读用 get
d.get("b") # 仅返回 None,不写坑三:以为pop("k", v)的第二个参数是「默认保留值」。
# 适用于 Python 3.8+
# ❌ 它不是「取不到就用这个值代替原值」,而是「取不到就返回它」
d = {"a": 1}
print(d.pop("a", 0)) # 1,能取到就返回真值
print(d.pop("a", 0)) # 0,已删掉,返回默认值
# ✅ 想「有就删、没有忽略」就显式传默认值
d.pop("z", None)坑四:对空字典调用popitem()。
# 适用于 Python 3.8+
# ❌ 空字典上 popitem 抛 KeyError
d = {}
d.popitem() # KeyError: 'popitem(): dictionary is empty'
# ✅ 先判空,或用 pop 给默认值
if d:
d.popitem()
d.pop("missing", None)坑五:以为values()能直接比大小。
# 适用于 Python 3.8+
# ❌ 值视图之间永远不等
d = {"a": 1}
print(d.values() == d.values()) # False
# ✅ 转成 list 再比
print(list(d.values()) == list(d.values())) # True坑六:copy()之后修改嵌套结构。
# 适用于 Python 3.8+
# ❌ 浅拷贝只到第一层
d = {"cfg": {"verbose": False}}
e = d.copy()
e["cfg"]["verbose"] = True
print(d["cfg"]["verbose"]) # True,原字典也变了
# ✅ 要彻底隔离用 deepcopy
import copy
e = copy.deepcopy(d)坑七:在 3.9 以前用|合并字典。
# 适用于 Python 3.9+
# ❌ 3.8 及更早版本没有字典的 | 运算符,这行会抛 TypeError
# d = {"a": 1} | {"b": 2}
# ✅ 3.9+ 可以直接用 |;需要在旧版本跑就改用 {**a, **b}
d = {**{"a": 1}, **{"b": 2}}
print(d) # {'a': 1, 'b': 2}总结
| 方法 | 返回 | 是否改原字典 | 要点 |
|---|
get(k, d=None) | 值或默认 | 否 | 从不抛KeyError |
setdefault(k, d=None) | 值或默认 | 缺键时写 | 会插入 |
update(...) | None | 是 | 覆盖已有键 |
pop(k[, d]) | 被删的值 | 是 | 缺键可给默认 |
popitem() | (k, v) | 是 | 3.7 起 LIFO |
clear() | None | 是 | 清空 |
copy() | 新字典 | 否 | 浅拷贝 |
keys/values/items() | 视图 | 否 | 动态、可迭代 |
fromkeys(seq, v) | 新字典 | 否 | 可变值会共享 |
| `\ | /\ | =` | 新字典 /None |
记牢三组对比就不容易出错:get不写、setdefault会写;pop删指定键、popitem删末尾;|出新字典、|=改原字典。