03-字典
本章代码基于 Python 3.11+ 编写
字典是键值对映射的容器,通过键快速查找值,是 Python 中最高效的数据结构之一。
本章全面讲解 Python 字典的创建、访问、修改、遍历和高级用法。
概念铺垫
为什么需要字典?一个键值映射的需求场景
问题场景: 你在开发一个学生成绩查询系统,需要通过学号快速找到学生姓名和成绩。
不使用字典的方案(列表):
# 用列表存储(查找麻烦)
students: list[list[str | int]] = [
["001", "张三", 85],
["002", "李四", 92],
["003", "王五", 78]
]
# 查找学号 002 的学生
for student in students:
if student[0] == "002":
print(student[1]) # 输出:李四问题:
- 查找慢(需要遍历)
- 索引不直观(0、1、2代表什么?)
使用字典的解决方案:
# 用字典存储(键值映射)
students: dict[str, dict[str, int]] = {
"001": {"name": "张三", "score": 85},
"002": {"name": "李四", "score": 92},
"003": {"name": "王五", "score": 78}
}
# 直接通过键查找(O(1) 查询)
print(students["002"]["name"]) # 输出:李四这就是字典的价值:用"键"快速找到"值"。
字典解决了什么问题?
字典的本质是:建立键和值的映射关系,实现快速查找。
就像你用字典查单词,通过"单词"快速找到"释义"。
字典的优势:
- 快速查找:O(1) 时间复杂度,比列表快得多
- 语义清晰:用有意义的键而非数字索引
- 灵活映射:任意类型的键和值
- 去重能力:键自动去重
在Python中,字典是最常用的数据结构之一,特别适合需要快速查找的场景。
L1 理解层:会用
字典的最简用法
字典的核心操作:创建、访问、修改。
# 创建字典
student: dict[str, str | int] = {"name": "张三", "age": 18}
# 访问值
print(student["name"]) # 张三
# 修改值
student["age"] = 19
# 添加新键值对
student["city"] = "北京"
print(student) # {"name": "张三", "age": 19, "city": "北京"}这就是字典的基本用法。接下来我们详细学习字典的所有功能。
第一部分:字典基础
什么是字典
字典(Dictionary): 存储键值对的数据结构,通过键快速查找值。
语法结构:
┌─────────────────────────────────────────────────────────────┐
│ 字典语法 │
│ │
│ {键: 值, 键: 值, ...} │
│ ↑ ↑ │
│ key value │
│ │
│ 示例:{"name": "Alice", "age": 25} │
│ ↑ ↑ │
│ 键 值 │
│ │
│ ⚠️ 键的类型限制: │
│ ✅ 可用:str、int、float、tuple(不可变类型) │
│ ❌ 不可用:list、dict、set(可变类型) │
└─────────────────────────────────────────────────────────────┘最简示例
person = {"name": "张三", "age": 18}
print(person["name"]) # 张三关键代码解释
| 概念 | 含义 | 说明 |
|---|---|---|
| 键(key) | 唯一标识 | 必须是不可变类型 |
| 值(value) | 对应数据 | 可以是任意类型 |
person["name"] | 通过键访问值 | 返回 "张三" |
键的类型限制(核心)
键必须是可哈希类型: 不可变类型可作为键,可变类型不可作为键。
键类型规则:
┌─────────────────────────────────────────────────────────────┐
│ 可作为键的类型(不可变) │
│ ───────────────────────────── │
│ str "name", "age" │
│ int 1, 100, -5 │
│ float 3.14, 0.5 │
│ tuple (1, 2), ("a", "b") │
│ │
│ 不可作为键的类型(可变) │
│ ───────────────────────────── │
│ list ❌ [1, 2, 3] │
│ dict ❌ {"a": 1} │
│ set ❌ {1, 2, 3} │
│ │
│ ⚠️ 原因:可变类型改变后哈希值变化,无法定位 │
└─────────────────────────────────────────────────────────────┘最简示例
# ✅ 不可变类型可作为键
d = {"name": "Alice", 1: "one", (1, 2): "tuple"}
print(d["name"]) # Alice
# ❌ 可变类型不可作为键
# d = {[1, 2]: "list"} # TypeError: unhashable type: 'list'关键代码解释
| 类型 | 可作为键 | 原因 |
|---|---|---|
str, int, tuple | ✅ | 不可变,哈希值稳定 |
list, dict, set | ❌ | 可变,哈希值不稳定 |
创建字典的七种方式
字典有7种创建方式,从简单到高级,各有适用场景。
┌─────────────────────────────────────────────────────────────┐
│ 创建字典的七种方式对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方式 语法 适用场景 │
│ ───────────────────────────────────────────────────── │
│ 1. 花括号 {} 最常用、最直观 │
│ 2. 空字典 {} 或 dict() 初始化空容器 │
│ 3. dict() dict(k=v) 关键字参数创建 │
│ 4. 键值对列表 dict([...]) 从数据结构转换 │
│ 5. zip() dict(zip(k,v)) 合并两个列表 │
│ 6. 推导式 {k: v for ...} 批量生成/转换 │
│ 7. fromkeys() dict.fromkeys() 批量初始化相同值 │
│ │
└─────────────────────────────────────────────────────────────┘方式 1:花括号(最常用)
概念说明: 使用 {} 直接定义键值对,是最直观、最常用的创建方式。
# 单行定义
person = {"name": "Alice", "age": 25, "city": "Beijing"}
# 多行定义(提高可读性)
config = {
"host": "localhost",
"port": 8080,
"debug": True,
"timeout": 30
}
# 嵌套字典
student = {
"name": "张三",
"scores": {"math": 90, "english": 85},
"info": {"age": 18, "class": "一班"}
}关键代码说明:
| 代码 | 含义 | 说明 |
|---|---|---|
{} | 花括号 | 字典的字面量语法 |
"name": "Alice" | 键值对 | 键和值用冒号分隔 |
, | 分隔符 | 多个键值对用逗号分隔 |
方式 2:创建空字典
概念说明: 两种创建空字典的方式,功能相同。
# 方式 A:花括号(推荐,更简洁)
empty1 = {}
# 方式 B:dict() 函数
empty2 = dict()
# 验证:两者完全等价
print(empty1 == empty2) # True
print(type(empty1)) # dict
print(type(empty2)) # dict
# 实际应用:初始化后逐步添加
result = {} # 先创建空字典
result["status"] = "success"
result["data"] = [1, 2, 3]何时用哪种?
| 方式 | 推荐场景 | 原因 |
|---|---|---|
{} | 大多数场景 | 更简洁、更直观 |
dict() | 需要动态创建时 | 可接收参数 |
方式 3:dict() 关键字参数
概念说明: 使用 dict() 函数,通过关键字参数创建字典。
# 关键字参数方式
person = dict(name="Alice", age=25, city="Beijing")
# 等价于花括号
person2 = {"name": "Alice", "age": 25, "city": "Beijing"}
# 注意:键名自动成为字符串
config = dict(host="localhost", port=8080)
print(config) # {'host': 'localhost', 'port': 8080}
# 键是字符串 'host',不是变量 host与花括号的区别:
| 对比项 | 花括号 {} | dict()关键字 |
|---|---|---|
| 键的形式 | 任意表达式 | 必须是标识符 |
| 键的类型 | 可指定类型 | 自动转为字符串 |
| 可读性 | 高 | 中 |
| 适用场景 | 已知键名 | 键名符合标识符规则 |
方式 4:从键值对列表创建
概念说明: 将包含元组的列表转换为字典,适合从结构化数据创建。
# 从元组列表创建
pairs = [("name", "Alice"), ("age", 25), ("city", "Beijing")]
person = dict(pairs)
print(person) # {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
# 实际应用:解析配置文件
config_lines = [
("host", "localhost"),
("port", "8080"),
("debug", "true")
]
config = dict(config_lines)
# 从 CSV 数据创建
csv_data = [
("id", "001"),
("name", "张三"),
("score", "85")
]
record = dict(csv_data)转换过程图解:
键值对列表 → 字典:
┌─────────────────────────────────────────────────────────────┐
│ [("name", "Alice"), ("age", 25), ("city", "Beijing")] │
│ │
│ ↓ dict() 转换 │
│ │
│ {"name": "Alice", "age": 25, "city": "Beijing"} │
│ │
│ 每个元组:第一个元素 → 键,第二个元素 → 值 │
└─────────────────────────────────────────────────────────────┘方式 5:zip() 组合两个列表
概念说明: 将两个列表(键列表、值列表)组合成字典,适合批量创建。
# 基本用法
keys = ["name", "age", "city"]
values = ["Alice", 25, "Beijing"]
person = dict(zip(keys, values))
print(person) # {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
# 实际应用:从表头和数据行创建
headers = ["id", "name", "score"]
row = ["001", "张三", 85]
record = dict(zip(headers, row))
print(record) # {'id': '001', 'name': '张三', 'score': 85}
# 批量创建:学生成绩表
students = ["张三", "李四", "王五"]
scores = [85, 92, 78]
score_map = dict(zip(students, scores))
print(score_map) # {'张三': 85, '李四': 92, '王五': 78}zip() 工作原理:
zip() 组合过程:
┌─────────────────────────────────────────────────────────────┐
│ keys = ["name", "age", "city"] │
│ values = ["Alice", 25, "Beijing"] │
│ │
│ zip(keys, values) │
│ → [("name", "Alice"), ("age", 25), ("city", "Beijing")] │
│ │
│ dict(zip(...)) │
│ → {"name": "Alice", "age": 25, "city": "Beijing"} │
│ │
│ ⚠️ 注意:列表长度不同时,忽略多余元素 │
└─────────────────────────────────────────────────────────────┘长度不匹配的处理:
# keys 比 values 长
keys = ["a", "b", "c"]
values = [1, 2]
result = dict(zip(keys, values))
print(result) # {'a': 1, 'b': 2} # 'c' 被忽略
# values 比 keys 长
keys = ["a", "b"]
values = [1, 2, 3]
result = dict(zip(keys, values))
print(result) # {'a': 1, 'b': 2} # 3 被忽略方式 6:字典推导式
概念说明: 用推导式语法批量生成字典,适合从序列创建或有条件过滤。
语法结构:
┌─────────────────────────────────────────────────────────────┐
│ {键表达式: 值表达式 for 变量 in 可迭代对象 if 条件} │
│ │
│ 示例:{i: i**2 for i in range(5) if i > 0} │
│ ↑ ↑ ↑ ↑ ↑ │
│ 键 值 遍历变量 数据源 过滤条件 │
└─────────────────────────────────────────────────────────────┘# 基础示例:数字平方
squares = {i: i ** 2 for i in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# 带条件过滤:只保留偶数
even_squares = {i: i ** 2 for i in range(1, 10) if i % 2 == 0}
print(even_squares) # {2: 4, 4: 20, 6: 36, 8: 64}
# 字符串处理:单词长度映射
words = ["apple", "banana", "cherry"]
word_lengths = {word: len(word) for word in words}
print(word_lengths) # {'apple': 5, 'banana': 6, 'cherry': 6}
# 从列表转换:成绩等级映射
scores = [85, 92, 78, 60, 95]
grade_map = {
score: "A" if score >= 90 else "B" if score >= 80 else "C"
for score in scores
}
print(grade_map) # {85: 'B', 92: 'A', 78: 'C', 60: 'C', 95: 'A'}实际应用场景:
# 场景 1:数据清洗与转换
raw_prices = ["100", "200", "150"]
prices = {i: float(p) for i, p in enumerate(raw_prices)}
print(prices) # {0: 100.0, 1: 200.0, 2: 150.0}
# 场景 2:字典值转换
original = {"a": 1, "b": 2, "c": 3}
doubled = {k: v * 2 for k, v in original.items()}
print(doubled) # {'a': 2, 'b': 4, 'c': 6}
# 场景 3:条件筛选
scores = {"张三": 85, "李四": 60, "王五": 92}
passed = {name: score for name, score in scores.items() if score >= 60}
print(passed) # {'张三': 85, '李四': 60, '王五': 92}方式 7:fromkeys() 批量初始化
概念说明: 为多个键设置相同的默认值,适合批量初始化。
# 基本用法
keys = ["a", "b", "c"]
default = dict.fromkeys(keys, 0)
print(default) # {'a': 0, 'b': 0, 'c': 0}
# 不指定值(默认 None)
result = dict.fromkeys(["x", "y", "z"])
print(result) # {'x': None, 'y': None, 'z': None}
# 实际应用:初始化计数器
categories = ["views", "likes", "shares", "comments"]
stats = dict.fromkeys(categories, 0)
print(stats) # {'views': 0, 'likes': 0, 'shares': 0, 'comments': 0}
# 应用:配置默认值
settings = dict.fromkeys(["debug", "verbose", "quiet"], False)
print(settings) # {'debug': False, 'verbose': False, 'quiet': False}fromkeys() 工作原理:
fromkeys() 创建过程:
┌─────────────────────────────────────────────────────────────┐
│ dict.fromkeys(["a", "b", "c"], 0) │
│ │
│ 步骤 1:遍历键列表 │
│ ["a", "b", "c"] │
│ │
│ 步骤 2:为每个键设置相同值 │
│ "a" → 0 │
│ "b" → 0 │
│ "c" → 0 │
│ │
│ 结果:{"a": 0, "b": 0, "c": 0} │
└─────────────────────────────────────────────────────────────┘⚠️ 注意:可变默认值陷阱
# ❌ 危险:所有键指向同一个列表
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': []} # 只有 a 被修改创建方式选择指南
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 已知所有键值 | {} 花括号 | 最直观、最简洁 |
| 初始化空字典 | {} 花括号 | 更简洁 |
| 键名是标识符 | dict(k=v) | 写法自然 |
| 从两个列表合并 | dict(zip()) | 一行搞定 |
| 批量生成/转换 | 推导式 | 灵活、支持条件 |
| 批量初始化相同值 | fromkeys() | 简洁 |
| 从结构化数据转换 | dict(list) | 数据对接 |
键的类型限制
┌─────────────────────────────────────────────────────────────┐
│ 可作为键的类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 可哈希类型(可以作为键): │
│ • 整数、浮点数、字符串 │
│ • 元组(元素也必须可哈希) │
│ • 布尔值、None │
│ • frozenset │
│ │
│ ❌ 不可哈希类型(不能作为键): │
│ • 列表 │
│ • 字典 │
│ • 集合 │
│ • 包含不可哈希元素的元组 │
│ │
│ 示例: │
│ ✅ {1: "one", "name": "Alice", (0, 0): "origin"} │
│ ❌ {[1, 2]: "value"} # TypeError │
│ ❌ {{1, 2}: "value"} # TypeError │
│ │
└─────────────────────────────────────────────────────────────┘# 有效键类型
d = {
1: "integer key",
"name": "string key",
(0, 0): "tuple key",
3.14: "float key",
True: "bool key"
}
# 无效键类型
# d = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
# 元组作为键的实际应用
grid = {
(0, 0): "起点",
(1, 0): "东",
(0, 1): "北"
}第二部分:访问字典
基本访问
person = {"name": "Alice", "age": 25, "city": "Beijing"}
# 方式 1:使用方括号(键不存在会报错)
print(person["name"]) # Alice
print(person["age"]) # 25
# print(person["salary"]) # KeyError: 'salary'
# 方式 2:使用 get() 方法(推荐,更安全)
print(person.get("name")) # Alice
print(person.get("salary")) # None(键不存在返回 None)
print(person.get("salary", 0)) # 0(指定默认值)
# 方式 3:使用 setdefault()(不存在则添加)
hobby = person.setdefault("hobby", "reading")
print(hobby) # reading
print(person["hobby"]) # reading(已添加到字典)安全访问模式
person = {"name": "Alice", "age": 25}
# 模式 1:get() + 默认值
salary = person.get("salary", 0)
# 模式 2:检查键是否存在
if "salary" in person:
salary = person["salary"]
else:
salary = 0
# 模式 3:try-except
try:
salary = person["salary"]
except KeyError:
salary = 0
# 模式 4:setdefault()(需要添加默认值时)
salary = person.setdefault("salary", 0)
# 嵌套字典的安全访问
data = {"user": {"name": "Alice"}}
# 错误方式
# data["user"]["age"] # KeyError
# 正确方式
age = data.get("user", {}).get("age", 0)第三部分:修改字典
添加和修改
person = {"name": "Alice", "age": 25}
# 添加新键值对
person["city"] = "Beijing"
person["hobby"] = "reading"
# 修改已有键值对
person["age"] = 26
# update() - 批量更新
person.update({"city": "Shanghai", "salary": 50000})
# update() - 使用关键字参数
person.update(job="Engineer", level="Senior")
# update() - 使用键值对列表
person.update([("team", "Backend"), ("years", 5)])
# |= 运算符(Python 3.9+)
person |= {"company": "TechCorp"}删除元素
person = {"name": "Alice", "age": 25, "city": "Beijing", "hobby": "reading"}
# pop() - 删除并返回值
age = person.pop("age")
print(age) # 25
print(person) # {'name': 'Alice', 'city': 'Beijing', 'hobby': 'reading'}
# pop() 带默认值(键不存在时不报错)
salary = person.pop("salary", 0)
print(salary) # 0
# popitem() - 删除并返回最后一个键值对
last = person.popitem()
print(last) # ('hobby', 'reading')
# del 语句
del person["city"]
# clear() - 清空字典
person.clear()
print(person) # {}
# 删除方法对比
┌─────────────────────────────────────────────────────────────┐
│ 删除方法对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方法 返回值 键不存在时 │
│ ───────────────────────────────────────────── │
│ pop(key) 值 KeyError │
│ pop(key, d) 值或 d 返回 d │
│ popitem() 键值对 KeyError(空字典) │
│ del d[key] 无 KeyError │
│ clear() 无 - │
│ │
└─────────────────────────────────────────────────────────────┘第四部分:遍历字典
遍历方式
person = {"name": "Alice", "age": 25, "city": "Beijing"}
# 遍历键(默认行为)
for key in person:
print(key)
# 显式遍历键
for key in person.keys():
print(key)
# 遍历值
for value in person.values():
print(value)
# 遍历键值对(最常用)
for key, value in person.items():
print(f"{key}: {value}")
# 带索引遍历
for i, (key, value) in enumerate(person.items()):
print(f"{i}. {key}: {value}")字典视图
person = {"name": "Alice", "age": 25}
# keys()、values()、items() 返回视图对象
keys_view = person.keys()
values_view = person.values()
items_view = person.items()
print(type(keys_view)) # <class 'dict_keys'>
print(type(values_view)) # <class 'dict_values'>
print(type(items_view)) # <class 'dict_items'>
# 视图是动态的(反映字典变化)
person["city"] = "Beijing"
print(list(keys_view)) # ['name', 'age', 'city'](自动更新)
# 视图支持集合操作
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
print(d1.keys() & d2.keys()) # {'b'}(交集)
print(d1.keys() | d2.keys()) # {'a', 'b', 'c'}(并集)
print(d1.keys() - d2.keys()) # {'a'}(差集)
# 视图转列表
keys_list = list(person.keys())
values_list = list(person.values())
items_list = list(person.items())第五部分:字典合并
合并方式
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
# 方式 1:update()(修改原字典)
result = dict1.copy()
result.update(dict2)
print(result) # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
# 方式 2:** 解包(Python 3.5+)
result = {**dict1, **dict2}
print(result)
# 方式 3:| 运算符(Python 3.9+)
result = dict1 | dict2
print(result)
# 方式 4:|= 运算符(原地合并,Python 3.9+)
dict1 |= dict2
print(dict1) # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
# 键冲突时的行为
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
result = {**d1, **d2}
print(result) # {'a': 1, 'b': 3, 'c': 4}(d2 的值覆盖)
result = d1 | d2
print(result) # {'a': 1, 'b': 3, 'c': 4}第六部分:特殊字典类型
defaultdict
概念说明
defaultdict 是 collections 模块提供的字典子类,为不存在的键提供默认值。
from collections import defaultdict
# 使用 list 作为默认工厂
word_lengths = defaultdict(list)
words = ["apple", "banana", "cherry", "apricot", "blueberry"]
for word in words:
word_lengths[len(word)].append(word)
print(word_lengths)
# {5: ['apple'], 6: ['banana', 'cherry'], 7: ['apricot'], 9: ['blueberry']}
# 使用 int 作为默认工厂(计数器)
counter = defaultdict(int)
for char in "hello world":
counter[char] += 1
print(counter) # {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}
# 使用 set 作为默认工厂
pairs = defaultdict(set)
pairs["a"].add(1)
pairs["a"].add(2)
pairs["a"].add(1) # 重复,不会添加
print(pairs) # {'a': {1, 2}}
# 自定义默认值
def default_value():
return "N/A"
d = defaultdict(default_value)
print(d["missing"]) # N/A
# 使用 lambda
d = defaultdict(lambda: "unknown")
print(d["missing"]) # unknownCounter
from collections import Counter
# 创建计数器
text = "hello world"
counter = Counter(text)
print(counter) # Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
# 从列表创建
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
word_count = Counter(words)
print(word_count) # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
# 常用方法
print(word_count.most_common(2)) # [('apple', 3), ('banana', 2)]
print(word_count.most_common()) # 所有元素按频率排序
# 更新计数
word_count.update(["apple", "date"])
print(word_count) # apple: 4
# 减少计数
word_count.subtract(["apple", "apple"])
print(word_count) # apple: 2
# 算术运算
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2) # Counter({'a': 4, 'b': 3})
print(c1 - c2) # Counter({'a': 2})
print(c1 & c2) # Counter({'a': 1, 'b': 1})(交集,取最小)
print(c1 | c2) # Counter({'a': 3, 'b': 2})(并集,取最大)OrderedDict
from collections import OrderedDict
# Python 3.7+ 普通 dict 已保持顺序
# OrderedDict 提供额外功能
od = OrderedDict()
od["a"] = 1
od["b"] = 2
od["c"] = 3
# 移动到末尾
od.move_to_end("a")
print(list(od.keys())) # ['b', 'c', 'a']
# 移动到开头
od.move_to_end("a", last=False)
print(list(od.keys())) # ['a', 'b', 'c']
# 按插入顺序弹出
print(od.popitem()) # ('c', 3)(从末尾)
print(od.popitem(last=False)) # ('a', 1)(从开头)
# LRU 缓存实现示例
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)第七部分:字典性能
时间复杂度
┌─────────────────────────────────────────────────────────────┐
│ 字典操作时间复杂度 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 操作 平均情况 最坏情况 │
│ ───────────────────────────────────────────── │
│ 访问 d[key] O(1) O(n) │
│ d.get(key) O(1) O(n) │
│ d[key] = value O(1) O(n) │
│ del d[key] O(1) O(n) │
│ key in d O(1) O(n) │
│ len(d) O(1) O(1) │
│ d.keys() O(1) O(1) │
│ 遍历 O(n) O(n) │
│ │
│ 最坏情况:哈希冲突严重时 │
│ Python 的哈希表实现会自动处理冲突,最坏情况很少出现 │
│ │
└─────────────────────────────────────────────────────────────┘性能对比
import timeit
# 字典 vs 列表查找
data_dict = {i: i * 2 for i in range(10000)}
data_list = list(range(10000))
# 字典查找 O(1)
dict_time = timeit.timeit('9999 in data_dict', globals=globals(), number=10000)
print(f"字典查找: {dict_time:.6f}s")
# 列表查找 O(n)
list_time = timeit.timeit('9999 in data_list', globals=globals(), number=10000)
print(f"列表查找: {list_time:.6f}s")
# 字典查找快几个数量级!第八部分:常见错误
常见陷阱
# 陷阱 1:遍历时修改字典
d = {"a": 1, "b": 2, "c": 3}
# ❌ 错误:遍历时删除
# for key in d:
# if d[key] == 2:
# del d[key] # RuntimeError
# ✅ 正确:遍历副本或收集要删除的键
for key in list(d.keys()):
if d[key] == 2:
del d[key]
# 陷阱 2:可变默认值
def add_item(item, d={}): # ❌ 默认字典会被修改
d[item] = True
return d
print(add_item("a")) # {'a': True}
print(add_item("b")) # {'a': True, 'b': True}(不是 {'b': True})
# ✅ 正确
def add_item(item, d=None):
if d is None:
d = {}
d[item] = True
return d
# 陷阱 3:键的类型
d = {}
d[1] = "int"
d["1"] = "string"
d[True] = "bool" # True == 1,会覆盖 d[1]
print(d) # {1: 'bool', '1': 'string'}
# 陷阱 4:浮点数键
d = {}
d[0.1 + 0.2] = "value"
print(d[0.3]) # KeyError!0.1 + 0.2 != 0.3(浮点精度问题)L2 实践层:用好
最佳实践
字典方法选择指南
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 访问已知存在的键 | d[key] | 简洁,语义明确 |
| 访问可能不存在的键 | d.get(key, default) | 安全,不抛异常 |
| 访问并设置默认值 | d.setdefault(key, default) | 自动添加缺失键 |
| 批量更新 | d.update(other) | 合并多个键值对 |
| 删除并获取值 | d.pop(key, default) | 安全删除 |
| 清空字典 | d.clear() | 比 d = {} 更明确 |
| 合并两个字典 | `d1 | d2`(Python 3.9+) |
| 原地合并 | `d1 | = d2` |
反模式:不要这样做
# ❌ 反模式 1:遍历字典时修改它
d = {"a": 1, "b": 2, "c": 3}
for key in d:
del d[key] # RuntimeError: dictionary changed size during iteration
# ✅ 正确做法:遍历副本
for key in list(d.keys()):
del d[key]# ❌ 反模式 2:用 `[]` 访问可能不存在的键
config = {"host": "localhost"}
port = config["port"] # KeyError: 'port'
# ✅ 正确做法:用 get() 提供默认值
port = config.get("port", 8080)# ❌ 反模式 3:可变默认参数
def process(data, cache={}): # 字典被共享!
cache[data] = True
return cache
process("a") # {'a': True}
process("b") # {'a': True, 'b': True} ← 不是 {'b': True}
# ✅ 正确做法:用 None
def process(data: str, cache: dict[str, bool] | None = None) -> dict[str, bool]:
if cache is None:
cache = {}
cache[data] = True
return cache# ❌ 反模式 4:频繁用 `if key in d: value = d[key]`
if "name" in person:
name = person["name"]
else:
name = "Unknown"
# ✅ 正确做法:一行搞定
name = person.get("name", "Unknown")# ❌ 反模式 5:用列表存储键值映射(查找慢)
mapping = [["key1", "value1"], ["key2", "value2"]]
for pair in mapping:
if pair[0] == "key1":
value = pair[1] # O(n)
# ✅ 正确做法:用字典
mapping = {"key1": "value1", "key2": "value2"}
value = mapping["key1"] # O(1)# ❌ 反模式 6:fromkeys() 使用可变默认值
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': []}适用场景
| 场景 | 是否推荐字典 | 原因 |
|---|---|---|
| 快速查找(键→值) | ✅ 推荐 | O(1) 查找 |
| 存储配置/设置 | ✅ 推荐 | 键名语义清晰 |
| 统计计数 | Counter | 专为计数设计 |
| 分组数据 | defaultdict | 自动创建默认值 |
| JSON 数据 | ✅ 推荐 | 天然映射 |
| 需要保持插入顺序 | ✅ 推荐(Python 3.7+) | 已自动保持 |
| 有序操作(移动元素) | OrderedDict | move_to_end 方法 |
| 只存储唯一元素 | ❌ 用 set | 字典有值,set 只存键 |
| 位置索引访问 | ❌ 用 list | 字典用键访问 |
性能专项:get() vs [] 安全访问
import timeit
d = {"key": "value"}
# [] 直接访问(键存在时)
print(timeit.timeit("d['key']", globals={'d': d}, number=10000000))
# ~0.15s — 最快,但键不存在时抛 KeyError
# get() 安全访问
print(timeit.timeit("d.get('key')", globals={'d': d}, number=10000000))
# ~0.25s — 慢 ~60%,但安全
# get() 带默认值
print(timeit.timeit("d.get('missing', None)", globals={'d': d}, number=10000000))
# ~0.28s
# 结论:确定键存在用 [],不确定用 get()defaultdict 模式实战
from collections import defaultdict
# ❌ 反模式:手动检查 + 初始化
word_groups = {}
for word in ["apple", "banana", "apricot"]:
first = word[0]
if first not in word_groups:
word_groups[first] = []
word_groups[first].append(word)
# ✅ 正确:defaultdict 自动初始化
word_groups = defaultdict(list)
for word in ["apple", "banana", "apricot"]:
word_groups[word[0]].append(word)
# ✅ 常用模式:
# defaultdict(list) — 分组
# defaultdict(int) — 计数
# defaultdict(set) — 唯一集合
# defaultdict(dict) — 嵌套字典
# ❌ 反模式:用 dict.get() 模拟 defaultdict
counts = {}
for char in "hello":
counts[char] = counts.get(char, 0) + 1 # 每次调用 get()
# ✅ 正确
counts = defaultdict(int)
for char in "hello":
counts[char] += 1 # 简洁、更快字典推导式 vs dict() + 生成器
# ✅ 字典推导式:可读性最好
squares = {x: x ** 2 for x in range(100)}
# ⚠️ dict() + 生成器:等价但稍慢
squares = dict((x, x ** 2) for x in range(100))
# ✅ 字典推导式带条件过滤
even_squares = {x: x ** 2 for x in range(20) if x % 2 == 0}
# ❌ 反模式:先用列表推导再转字典
pairs = [(x, x ** 2) for x in range(100)]
squares = dict(pairs) # 多一步中间列表L3 专家层:深入
底层原理:哈希表
Python 如何实现字典
字典在 Python 中使用哈希表实现:
哈希表内部结构(Python 3.11+ 紧凑实现):
┌─────────────────────────────────────────────────────────────┐
│ 哈希表原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Python 3.11+ 紧凑字典结构: │
│ │
│ indices 数组(索引表): │
│ ┌────────────────────────────────────────────┐ │
│ │ [空] [空] [1] [空] [空] [0] [空] [2] │ │
│ └────────────────────────────────────────────┘ │
│ ↑ ↑ ↑ │
│ hash%8=2 hash%8=5 hash%8=7 │
│ │
│ entries 数组(条目表): │
│ ┌────────────────────────────────────────────┐ │
│ │ [0] hash(key1), key1, value1 │ │
│ │ [1] hash(key2), key2, value2 │ │
│ │ [2] hash(key3), key3, value3 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ 查找流程: │
│ 1. hash(key) → 哈希值 │
│ 2. hash % size → 索引位置 │
│ 3. indices[位置] → entries 索引 │
│ 4. entries[索引] → 键值对 │
│ 5. 比较 key 是否匹配 │
│ │
│ 冲突处理:开放寻址法 + 线性探测 │
│ • 位置被占用 → 找下一个空位 │
│ • Python 自动处理冲突 │
│ │
│ 内存优势(Python 3.11+): │
│ • indices 只存索引(1字节/元素) │
│ • entries 存完整数据 │
│ • 比 Python 3.6 前节省 ~25% 内存 │
│ │
└─────────────────────────────────────────────────────────────┘哈希冲突演示
# 查看对象的哈希值
print(hash("name")) # 例如:-123456789
print(hash("age")) # 例如:987654321
# 不同对象可能有相同哈希(冲突)
class BadKey:
def __hash__(self):
return 1 # 强制所有实例返回相同哈希
k1, k2 = BadKey(), BadKey()
d = {k1: "value1"}
d[k2] = "value2" # 冲突!但 Python 自动处理
print(d) # {<BadKey>: 'value1', <BadKey>: 'value2'}性能考量
import sys
import timeit
# 内存:字典比列表更紧凑
d = {i: i for i in range(100)}
lst = list(range(100))
print(sys.getsizeof(d)) # ~360 bytes
print(sys.getsizeof(lst)) # ~904 bytes(包含预留)
# 但字典每个键值对额外占用约 72 bytes
# 列表每个元素约 8 bytes
# 查找速度对比
d = {i: i for i in range(10000)}
lst = list(range(10000))
# 字典 O(1)
dict_time = timeit.timeit('9999 in d', globals=globals(), number=100000)
# ~0.005s
# 列表 O(n)
list_time = timeit.timeit('9999 in lst', globals=globals(), number=100000)
# ~5s(慢 1000 倍!)| 操作 | 时间复杂度 | 说明 |
|---|---|---|
d[key] 查找 | O(1) 平均 | 哈希定位 |
d[key] = v 插入 | O(1) 平均 | 哈希定位后写入 |
key in d 成员检查 | O(1) 平均 | 比 list 快 1000x |
del d[key] 删除 | O(1) 平均 | 标记删除位置 |
for k in d 遍历 | O(n) | 访问所有键 |
设计动机
| 设计选择 | 原因 | 影响 |
|---|---|---|
| 哈希表 | 快速查找 | O(1) 平均 |
| 开放寻址 | 内存紧凑 | 比链表法省内存 |
| 保持插入顺序(3.7+) | 实际需求频繁 | 无需 OrderedDict |
| 紧凑结构(3.11+) | 内存优化 | 节省 25% |
Python 字典演进史
字典实现演进:
┌─────────────────────────────────────────────────────────────┐
│ Python 版本 字典实现 │
│ ───────────────────────────────────────────── │
│ < 3.6 传统哈希表(无序) │
│ 3.6 紧凑字典(有序,但非官方保证) │
│ 3.7+ 紧凑字典(有序,官方保证) │
│ 3.11+ 超紧凑字典(更省内存) │
│ │
│ 关键变化: │
│ • 3.6 前:entries 直接存储在主数组 │
│ • 3.6+:分离 indices 和 entries │
│ • 3.11+:indices 使用更小类型(1-4 bytes) │
│ │
│ 内存节省: │
│ Python 3.5 → 3.11:约节省 25% 内存 │
└─────────────────────────────────────────────────────────────┘知识关联
字典知识关联图:
┌─────────────────┐
│ 哈希函数 │
│ hash() │
└─────────────────┘
│
▼
┌───────────┐ ┌─────────────────┐ ┌───────────┐
│ 可哈希对象│────→│ 字典 dict │────→│ 集合 │
│ tuple/str│ │ 键值对映射 │ │ set │
└───────────┘ └─────────────────┘ └───────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│defaultdict│ │ Counter │ │OrderedDict│
│ 默认值 │ │ 计数 │ │ 有序 │
└───────────┘ └───────────┘ └───────────┘
对比:
dict ──→ O(1) 查找 ──→ 键值映射
list ──→ O(n) 查找 ──→ 位置索引
set ──→ O(1) 查找 ──→ 只存键,无值字节码验证:dict 查找底层
import dis
# 字典查找 — BINARY_SUBSCR + 哈希定位
def dict_lookup():
d = {"a": 1, "b": 2}
return d["a"]
dis.dis(dict_lookup)
# 输出:
# BUILD_MAP 2 创建字典(预分配 2 个槽位)
# LOAD_CONST ('a', 1) 加载键值对
# STORE_FAST d
# LOAD_FAST d
# LOAD_CONST 'a'
# BINARY_SUBSCR 字典下标查找 ← 触发哈希表查询
# get() 查找 — 额外的函数调用
def dict_get():
d = {"a": 1}
return d.get("b", 0)
dis.dis(dict_get)
# 输出:
# LOAD_METHOD get ← 方法查找
# LOAD_CONST 'b'
# LOAD_CONST 0
# CALL_METHOD 2 ← 函数调用
# 比 d["a"] 多 2 条指令,慢 ~40%紧凑字典内存验证(Python 3.6+)
import sys
d = {}
prev = sys.getsizeof(d)
last_change = 0
for i in range(20):
d[i] = i
cur = sys.getsizeof(d)
if cur != prev:
print(f"items={len(d):2d} size={cur:4d} bytes 扩容 (last={last_change})")
last_change = len(d)
prev = cur
# 典型输出(Python 3.11+):
# items= 1 size= 232 bytes 扩容
# items= 6 size= 360 bytes 扩容
# items=11 size= 640 bytes 扩容
# items=16 size= 640 bytes 扩容
# 稠密存储:indices (1-2 bytes/elem) + entries (24 bytes/elem)
# Python 3.5 前:每 entry 占用 72 bytes,3.11 压缩到 ~24+ bytes/entry
# 与列表内存对比
lst = [None] * 1000
d = {i: None for i in range(1000)}
print(f"列表 1000 元素: {sys.getsizeof(lst)} bytes")
print(f"字典 1000 键值对: {sys.getsizeof(d)} bytes")
# 字典约 2-3 倍内存(哈希表开销),但 O(1) 查找 vs O(n)本章小结
┌─────────────────────────────────────────────────────────────┐
│ 字典 知识要点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 创建: │
│ ✓ {"key": "value"}、dict()、zip()、推导式 │
│ │
│ 访问: │
│ ✓ d[key](不存在报错) │
│ ✓ d.get(key, default)(推荐) │
│ ✓ d.setdefault(key, default) │
│ │
│ 修改: │
│ ✓ d[key] = value、d.update() │
│ ✓ d |= other_dict(Python 3.9+) │
│ │
│ 删除: │
│ ✓ d.pop(key)、d.popitem()、del d[key]、d.clear() │
│ │
│ 遍历: │
│ ✓ d.keys()、d.values()、d.items() │
│ ✓ 视图对象支持集合操作 │
│ │
│ 合并: │
│ ✓ {**d1, **d2}、d1 | d2(Python 3.9+) │
│ │
│ 特殊字典: │
│ ✓ defaultdict:自动创建默认值 │
│ ✓ Counter:计数器 │
│ ✓ OrderedDict:有序字典(额外方法) │
│ │
│ 性能: │
│ ✓ 查找、插入、删除平均 O(1) │
│ │
└─────────────────────────────────────────────────────────────┘