跳转到内容
新建笔记

Python 字典:键、顺序、视图与更新

字典 dict 把可哈希的键映射到值。Python 3.11 中字典保持插入次序;迭代和 in 默认面向键,值可以重复。判断键能否使用,应看哈希与相等规则,不能简单等同于“这个对象不可变”。

下列程序分别可独立运行,示例中的 assert 用于验证结果,运行时不要使用 -O。

创建、访问、更新与键的唯一性

跳转到“创建、访问、更新与键的唯一性”
assert {} == dict()
student = {"Name": "Runoob", "Age": 7, "Class": "First"}
assert student["Age"] == 7
student["Age"] = 8
assert list(student) == ["Name", "Age", "Class"]
assert "Age" in student and 8 not in student and 8 in student.values()
assert dict([("a", 1), ("a", 2)]) == {"a": 2}
assert student.update({"Score": 90}, Class="Second") is None
assert student["Score"] == 90 and student["Class"] == "Second"
assert list(student) == ["Name", "Age", "Class", "Score"]
del student["Name"]
student["Name"] = "Again"
assert list(student)[-1] == "Name"
same_key = {1: "integer", True: "boolean", 1.0: "float"}
assert len(same_key) == 1 and same_key[1] == "float"
assert hash(1) == hash(True) == hash(1.0)
mapping = {("sensor", 1): "ready", frozenset({1, 2}): "group"}
assert mapping[("sensor", 1)] == "ready"
for key in ([1], ([1], 2)):
try:
mapping[key] = "invalid"
except TypeError:
pass
else:
raise AssertionError("unhashable dictionary key was accepted")
print("dictionary keys and insertion-order checks passed")

重复给同一个键赋值会覆盖其值,字典不会同时保留两份相同键的条目。更新既有键不把它移到末尾;删除再插入则形成新的插入位置。整数 1、布尔值 True 和浮点数 1.0 相等且哈希相同,因此这里代表同一个键。

包含列表的元组仍不可哈希;自定义对象则需要其哈希和相等行为保持符合契约。更多边界见元组及比较与哈希。

查询和删除方法的区别

跳转到“查询和删除方法的区别”
方法行为缺失键或空字典
get(key, default=None)返回对应值,否则返回 default不插入新条目
setdefault(key, default=None)已存在则返回原值,否则插入并返回 default会改变字典
pop(key, default)删除并返回对应值无 default 时缺失键抛出 KeyError;default 只能有一个
popitem()删除并返回最后插入的一对键和值空字典抛出 KeyError,不是随机删除
clear()清空现有字典返回 None,其他别名会看到变化
update(other, **kwargs)从映射或键值对序列更新,再应用关键字项返回 None,冲突时后面的值覆盖前面的值
copy()浅拷贝外层字典嵌套对象仍然共享
fromkeys(iterable, value=None)用给定键建立新字典所有键最初引用同一个 value
d = {"a": 1}
assert d.get("missing") is None and "missing" not in d
assert d.get("missing", 99) == 99
assert d.setdefault("b", 2) == 2 and d == {"a": 1, "b": 2}
assert d.setdefault("b", 99) == 2
assert d.pop("a") == 1
assert d.pop("missing", 99) == 99
try:
d.pop("missing")
except KeyError:
pass
else:
raise AssertionError("missing pop without default succeeded")
d["c"] = 3
d["b"] = 20
assert d.popitem() == ("c", 3)
assert d.popitem() == ("b", 20)
try:
d.popitem()
except KeyError:
pass
else:
raise AssertionError("popitem succeeded on an empty dictionary")
d.update([("x", 1), ("y", 2)], x=3)
assert d == {"x": 3, "y": 2}
alias = d
assert d.clear() is None and alias == {}
del d
assert alias == {}
print("dictionary lookup and removal checks passed")

popitem() 的后进先出顺序从 Python 3.7 起得到保证。del d 删除变量绑定;它与 d.clear() 修改对象是不同操作。还要注意:方法参数在调用前就会求值,不能把 setdefault(key, expensive()) 当成只在缺失键时才调用 expensive 的惰性接口。

原文的 fromkeys 很容易在可变默认值上造成误解。下面既验证共享结果,也给出每个键各自创建列表的方式。

shared = dict.fromkeys(["a", "b"], [])
shared["a"].append(1)
assert shared == {"a": [1], "b": [1]}
assert shared["a"] is shared["b"]
separate = {key: [] for key in ["a", "b"]}
separate["a"].append(1)
assert separate == {"a": [1], "b": []}
assert separate["a"] is not separate["b"]
shallow = separate.copy()
assert shallow is not separate and shallow["a"] is separate["a"]
shallow["a"].append(2)
shallow["new"] = []
assert separate["a"] == [1, 2] and "new" not in separate
calls = []
def make_default():
calls.append("called")
return []
d = {"existing": [3]}
assert d.setdefault("existing", make_default()) == [3]
assert calls == ["called"]
sentinel = object()
assert {"value": None}.get("value", sentinel) is None
assert {}.get("value", sentinel) is sentinel
print("dictionary defaults and shallow-copy checks passed")

需要区分“没有这个键”和“这个键对应 None”时,使用不会与业务值混淆的标记对象,或者先明确检查成员关系。深拷贝的范围另见浅拷贝与深拷贝。

keys()、values()、items() 返回动态视图,不是列表。若需要当时的外层条目快照,可以显式用 list(...) 保存;其中的可变值仍是引用。字典和这些视图都按插入次序遍历,reversed() 按相反次序遍历。

d = {"a": 1, "b": 2}
keys, values, items = d.keys(), d.values(), d.items()
snapshot = list(items)
d["c"] = 3
assert list(keys) == ["a", "b", "c"]
assert list(values) == [1, 2, 3]
assert list(items) == [("a", 1), ("b", 2), ("c", 3)]
assert snapshot == [("a", 1), ("b", 2)]
assert list(reversed(d)) == ["c", "b", "a"]
assert list(reversed(items)) == [("c", 3), ("b", 2), ("a", 1)]
assert keys & {"b", "z"} == {"b"}
assert ("a", 1) in items
iterator = iter(d)
assert next(iterator) == "a"
d["new"] = 4
try:
next(iterator)
except RuntimeError:
pass
else:
raise AssertionError("size-changing iteration bug was not detected")
for key in list(d):
if key == "new":
del d[key]
assert list(d) == ["a", "b", "c"]
print("dictionary view and iteration checks passed")

遍历期间添加或删除条目可能引发异常或漏过条目,不应依赖某次运行碰巧可用。需要删除选中的键时,可以像例子一样遍历已取得的键列表,或先收集需要删除的键。视图规则见 Python 字典视图。

合并、格式化与特殊名称

跳转到“合并、格式化与特殊名称”

Python 3.9 起,普通字典可以用 left | right 创建合并结果,冲突键采用右侧值;|= 原地更新,且像 update 一样可以接收键值对序列。这里的合并仍是浅层操作。

import pickle
import sys
left, right = {"a": 1, "same": "left"}, {"b": 2, "same": "right"}
merged = left | right
assert merged == {"a": 1, "same": "right", "b": 2}
assert left["same"] == "left"
left |= [("extra", 3)]
assert left["extra"] == 3
try:
right | [("extra", 3)]
except TypeError:
pass
else:
raise AssertionError("ordinary dict union accepted a pair list")
d = {"a": 1}
assert "{a}".format(**d) == "1"
assert format(d, "") == str(d)
try:
format(d, ".2f")
except TypeError:
pass
else:
raise AssertionError("dictionary accepted numeric format")
assert d.__class__ is dict and isinstance(dict.__doc__, str)
assert "items" in dir(d) and len(d) == 1
assert sys.getsizeof(d) >= d.__sizeof__()
assert pickle.loads(pickle.dumps(d, protocol=2)) == d
print("dictionary union, formatting and metadata checks passed")

"{a}".format(**d) 调用的是字符串的格式化方法,把字典展开为关键字参数;不能据此认为字典的 __format__ 支持任意数值格式。原表涉及的特殊名称可按下表查找。

常用操作对应名称说明
类型、文档、属性目录__class__、__doc__、__dir__前两个是属性,日常用 type 和 dir
长度、对象内存大小__len__、__sizeof__len 数条目;sys.getsizeof 不递归统计所有键和值
表示与格式化__repr__、__str__、__format__不等同于对另一个模板字符串执行 format
下标读写、删除__getitem__、__setitem__、__delitem__以键定位;不是位置索引
遍历、反向遍历、成员判断__iter__、__reversed__、__contains__默认面向键
相等、不等__eq__、__ne__内容相等不要求插入次序相同;普通 dict 没有大小排序
pickle 重建__reduce_ex__常规代码通过 pickle 使用,不能恢复不可信字节

完整方法说明见 Python 字典类型,内存统计见 sys.getsizeof,序列化边界见特殊方法与对象协议。