Python 字典(dict):创建、方法、遍历、排序与 frozendict
字典(dict)是可变的“键—值”集合,按键查找几乎瞬时完成;用花括号创建,例如 {"咖啡": 250},或使用 dict() 函数。键必须唯一且可哈希,从 Python 3.7 起保留插入顺序;3.15 中新增了内置的不可变版本 frozendict。
Python 字典(dict)是“键—值”对的集合,无论里面有多少条记录,按键查找值几乎都是瞬间完成的。要统计各类别的支出、保存设置,或者按 id 快速查找用户,都会用到字典。下面是使用 python dict 所需的全部内容:创建、方法、遍历、排序、合并,以及 3.15 中的新特性 frozendict。
Python 字典是什么
字典保存的是键值对,每个键都是唯一的:对已存在的键再次赋值,旧值会被替换。从实现上看,它是一张哈希表,因此按键查找、添加和删除平均都是常数时间,而不需要逐个遍历。
键必须是可哈希的对象:字符串、数字、由不可变值组成的元组。列表不能作为键,否则会得到 TypeError: unhashable type: 'list'。而值可以是任何东西,包括其他字典和列表。
从 Python 3.7 开始,字典保证记住插入顺序:遍历时,键会按照添加的先后顺序出现。
如何创建字典
创建字典有好几种方式,实际代码中都会遇到。
spent = {"餐饮": 1200, "打车": 450} # 字面量
empty = {} # 空字典
also_empty = dict() # 用 dict 函数,效果相同
user = dict(name="安娜", age=30) # 关键字参数
pairs = dict([("a", 1), ("b", 2)]) # 从一组键值对创建
keys = dict.fromkeys(["x", "y"], 0) # {'x': 0, 'y': 0}
注意:空的花括号 {} 创建的是空字典,而不是集合。空集合只能用 set() 创建。
用两个列表构造字典的一个便捷方法是 zip:
names = ["餐饮", "打车", "电影"]
sums = [1200, 450, 600]
spent = dict(zip(names, sums))
print(spent) # {'餐饮': 1200, '打车': 450, '电影': 600}
按键访问:d[k] 与 get
用方括号读取值。如果键不存在,Python 会抛出 KeyError。要避免这种错误,使用 get 方法:它会返回 None 或指定的默认值。
spent = {"餐饮": 1200}
print(spent["餐饮"]) # 1200
print(spent.get("电影")) # None
print(spent.get("电影", 0)) # 0
spent["电影"] # KeyError: '电影'
要检查字典中是否存在某个键,使用运算符 in:"餐饮" in spent 返回 True。它检查的是键,而不是值。检查值应写成 1200 in spent.values()。
添加、修改与删除
添加或更新一条记录只需一行赋值。配合 get 做计数非常方便:
spent = {}
for category, amount in [("餐饮", 250), ("打车", 300), ("餐饮", 400)]:
spent[category] = spent.get(category, 0) + amount
print(spent) # {'餐饮': 650, '打车': 300}
删除键可以使用 del d[k](如果键不存在会抛出 KeyError),或者 d.pop(k, default),后者还会返回对应的值。清空整个字典使用 d.clear()。字典的长度,即键值对的数量,用 len(d) 获得。
字典方法速查
| 方法 | 作用 |
|---|---|
get(k, default) | 按键取值,不存在时返回 default,不报错 |
setdefault(k, default) | 返回值;如果键不存在,先写入 default |
update(other) | 添加另一个字典的键值对,并覆盖已有的键 |
pop(k, default) | 删除键并返回值 |
popitem() | 删除并返回最后添加的键值对 |
keys()、values()、items() | 键、值以及键值对的视图 |
copy() | 浅复制 |
clear() | 清空字典 |
fromkeys(keys, value) | 用相同的值创建新字典 |
setdefault:用列表填充字典
当值是列表并且需要分组时,setdefault 非常好用:
groups = {}
for item, cat in [("咖啡", "餐饮"), ("地铁", "交通"), ("披萨", "餐饮")]:
groups.setdefault(cat, []).append(item)
print(groups) # {'餐饮': ['咖啡', '披萨'], '交通': ['地铁']}
update:更新字典
update 原地修改字典:新的键被添加,已有的键被覆盖。
cfg = {"debug": False, "lang": "zh"}
cfg.update({"debug": True, "theme": "dark"})
print(cfg) # {'debug': True, 'lang': 'zh', 'theme': 'dark'}
defaultdict:带默认值的字典
collections.defaultdict 是 dict 的子类:访问不存在的键时,它会通过工厂函数自动创建一个值。这样统计和分组的代码会更简短。
from collections import defaultdict
spent = defaultdict(int)
for cat, amount in [("餐饮", 250), ("打车", 300), ("餐饮", 400)]:
spent[cat] += amount
print(dict(spent)) # {'餐饮': 650, '打车': 300}
groups = defaultdict(list)
groups["餐饮"].append("咖啡")
如果只需要统计出现次数,还可以使用 collections.Counter:Counter(["a", "b", "a"]) 得到 Counter({'a': 2, 'b': 1})。
遍历字典:for 循环
在 for 循环中遍历字典默认取的是键。要同时得到键和值,遍历 items()——这是最常见的写法。
spent = {"餐饮": 650, "打车": 300}
for cat in spent: # 键
print(cat)
for amount in spent.values(): # 值
print(amount)
for cat, amount in spent.items(): # 键值对
print(f"{cat}: {amount} ₽")
在遍历过程中不能改变字典的大小,否则会得到 RuntimeError: dictionary changed size during iteration。如果需要删除部分键,应遍历它的副本:for k in list(d):。
输出字典
print(d) 会把字典打印在一行上。对于较大或嵌套的字典,使用 pprint 模块或 json.dumps(d, ensure_ascii=False, indent=2) 更好——它会以缩进的形式输出数据,并且中文不会被转义。
字典排序
字典本身不能原地排序,它没有 sort 方法。对字典排序的做法是:用 sorted 对键值对排序,然后用结果创建一个新字典。
spent = {"打车": 300, "餐饮": 650, "电影": 120}
by_key = dict(sorted(spent.items()))
print(by_key) # {'打车': 300, '电影': 120, '餐饮': 650}
by_value = dict(sorted(spent.items(), key=lambda p: p[1], reverse=True))
print(by_value) # {'餐饮': 650, '打车': 300, '电影': 120}
如果只需要前 N 个类别,就不必再转换回字典:sorted(spent.items(), key=lambda p: p[1], reverse=True)[:2] 返回的是键值对列表。
按字段对字典列表排序:sorted(users, key=lambda u: u["age"])。
合并字典
从 Python 3.9 起,字典可以用运算符 | 合并,|= 则原地更新。键冲突时,右边的操作数优先。
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b) # {'x': 1, 'y': 20, 'z': 3}
print({**a, **b}) # 同样的结果,使用解包
a |= b # 相当于 a.update(b)
注意:字典之间不能用 a + b 相加,会得到 TypeError。如果确实需要把相同键的值加起来,使用 Counter:Counter(a) + Counter(b)。
字典解包
两个星号 ** 可以把字典解包为函数的关键字参数,或者解包到另一个字典中:
def greet(name, city):
return f"{name} 来自 {city}"
data = {"name": "安娜", "city": "喀山"}
print(greet(**data)) # 安娜 来自 喀山
字典推导式(dict comprehension)
字典推导式可以用一行代码从任何序列构建字典:
squares = {n: n * n for n in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
big = {k: v for k, v in spent.items() if v > 200}
inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(inverted) # {1: 'a', 2: 'b'}
最后一个例子展示了如何交换键和值。它只在值唯一且可哈希时有效,否则一部分键值对会丢失。
嵌套字典
值可以是另一个字典——这样可以描述 API 的 JSON 响应,或者“用户字典的字典”这样的结构。
users = {
101: {"name": "安娜", "skills": ["python", "sql"]},
102: {"name": "奥列格", "skills": ["go"]},
}
print(users[101]["skills"][0]) # python
print(users.get(103, {}).get("name")) # None,不会抛出 KeyError
复制字典:副本与第二个名字
语句 b = a 并不会复制字典,而是给同一个对象起了第二个名字。通过 b 修改,在 a 中也能看到变化。把字典传给函数时也是如此:函数拿到的是共享对象的引用,可能会悄悄地改坏它。
import copy
a = {"limits": [0, 500]}
b = a.copy() # 浅复制
b["limits"].append(1000) # 嵌套列表是共享的!
print(a) # {'limits': [0, 500, 1000]}
c = copy.deepcopy(a) # 完全独立的副本
比较字典
运算符 == 按内容比较字典:只有所有键值对都相同才相等。键的顺序不影响结果:{"a": 1, "b": 2} == {"b": 2, "a": 1} 为 True。要找出共同的键,可以对视图做集合运算:a.keys() & b.keys()。
带类型的字典:注解
字典的注解写作 dict[str, int]。如果字典有固定的一组字段,类型各不相同,可以使用 typing 模块中的 TypedDict:
from typing import TypedDict
class User(TypedDict):
name: str
age: int
u: User = {"name": "安娜", "age": 30}
注解在运行时不会被检查,它们是给 IDE 和 mypy 等分析工具用的。
不可变字典:Python 3.15 中的 frozendict
Python 3.15 引入了内置类型 frozendict(PEP 814):无需导入,尝试修改它会立刻抛出 TypeError。以前,不可变字典只能用 types 模块中的包装器或第三方包来模拟。它正适合保存共享的设置——任何函数都不应悄悄地修改它们。
config = frozendict(min_amount=0, max_amount=500)
config["max_amount"] = 1000
# TypeError: 'frozendict' object does not support item assignment
strict = config | {"max_amount": 300} # 得到新的 frozendict,原对象不变
cache = {config: "五月报告"} # 可哈希,可以作为键
根据课程材料,需要了解以下几点:
frozendict继承自object,而不是dict,因此isinstance(cfg, dict)返回False。要判断“任意字典”,使用collections.abc.Mapping或isinstance(x, (dict, frozendict))。- 只有当键和值都可哈希时,它才是可哈希的:
frozendict(a=[1])可以创建,但hash()会失败。它的不可变性是浅层的,与元组一样。 - 它保存顺序,但
==不考虑顺序。 json.dumps、pickle和copy都支持 frozendict。
常见错误
- 读取时出现 KeyError — 使用
get或先用in检查。 - 用列表作为键 — 把它转换为元组:
d[tuple(lst)]。 - 在循环中修改字典 — 遍历
list(d)。 - 以为赋值会产生副本 — 使用
copy()或deepcopy。 - 用
{}表示空集合 — 它是空字典。
小结
Python dict 是语言中最重要的数据结构:按键快速查找,保留插入顺序,并提供便捷的方法 get、setdefault、update,通过 items() 遍历,用 sorted 排序,用运算符 | 合并。注意复制行为和可哈希的键;如果需要不可变的设置,在 Python 3.15 中使用 frozendict。字典、列表、元组和集合在课程《Python 3.15:从新手到专家》中结合支出记录器项目进行了实践讲解。
常见问题
字典中的值可以是不同类型吗?
可以,同一个字典中的值可以是任何类型,并且彼此不同:数字、字符串、列表、其他字典。只有键有限制——它们必须是可哈希的。
如何获取字典中元素的个数?
函数 len(d) 返回键值对的数量。空字典的长度为 0,在 if 条件中为假。
dict 和 defaultdict 有什么区别?
defaultdict 是 dict 的子类,访问不存在的键时不会抛出 KeyError,而是通过工厂函数(例如 int 或 list)创建值。其他方面它的行为与普通字典相同。
如何通过值找到键?
没有直接的方法,需要遍历:next((k for k, v in d.items() if v == value), None)。如果经常这样查找,可以用推导式构建反向字典 {v: k for k, v in d.items()}。
如何把字典保存到文件中?
通常使用 json 模块:以写入模式打开文件后执行 json.dump(d, f, ensure_ascii=False, indent=2),读取时使用 json.load(f)。保存后,键会变成字符串。