Python 3 教程

Python3 字典

在 Python 3 中,字典 (Dictionary, dict) 是最核心、最强大的内置数据结构之一。它通过键值对 (Key-Value pairs) 的形式存储数据,底层基于高度优化的哈希表 (Hash Table) 实现。

字典不仅是 Python 开发者日常使用最频繁的工具,也是理解 Python 对象模型、命名空间(如 globals()locals())以及现代 Python 性能优化的关键。

以下是对 Python 3 字典的全面、深度解析,涵盖从基础语法、高级特性、底层原理到最佳实践的完整指南。(本文篇幅较长,旨在提供一份可作为生产环境参考手册的深度指南)


一、 字典的核心特性

  1. 键值对映射:通过唯一的“键 (Key)”来快速查找对应的“值 (Value)”。
  2. 可变性 (Mutable):字典的内容可以动态地增加、删除或修改。
  3. 键的唯一性与可哈希性 (Hashable):字典的键必须是不可变且可哈希的类型(如字符串、数字、元组)。列表或字典不能作为键。
  4. 保持插入顺序 (Ordered)从 Python 3.7 开始,字典正式保证保持元素的插入顺序。(Python 3.6 中作为 CPython 的实现细节,3.7 写入语言规范)。

二、 字典的创建与初始化

1. 字面量创建 (最常用、最推荐)

使用花括号 {} 和冒号 :

纯文本
plaintext
# 空字典
empty_dict = {}

# 标准字典
user = {
    "name": "Alice",
    "age": 28,
    "is_active": True
}

# 键可以是不同类型的可哈希对象
mixed_keys = {
    1: "one",
    (2, 3): "tuple_key",  # 元组作为键
    3.14: "pi"
}

2. 使用 dict() 构造函数

纯文本
plaintext
# 从键值对序列(如列表包含元组)创建
pairs = [("name", "Bob"), ("age", 30)]
d1 = dict(pairs)  # {'name': 'Bob', 'age': 30}

# 使用关键字参数 (注意:键必须是合法的 Python 标识符,且只能是字符串)
d2 = dict(name="Charlie", age=25)  # {'name': 'Charlie', 'age': 25}

# 结合 zip 函数快速创建字典
keys = ["a", "b", "c"]
values = [1, 2, 3]
d3 = dict(zip(keys, values))  # {'a': 1, 'b': 2, 'c': 3}

3. 字典推导式 (Dict Comprehension) 🌟

与列表推导式类似,用于基于现有数据快速生成新字典,代码简洁且执行效率高。

纯文本
plaintext
# 基础用法:生成数字及其平方的映射
squares = {x: x**2 for x in range(5)}
# 结果: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 带条件过滤:只保留偶数的平方
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}

# 键值反转 (交换键和值)
original = {"a": 1, "b": 2, "c": 3}
reversed_dict = {v: k for k, v in original.items()}
# 结果: {1: 'a', 2: 'b', 3: 'c'}

三、 字典的增、删、改、查操作

1. 查找与访问 (Read)

  • d[key]:直接访问。如果键不存在,会抛出 KeyError。时间复杂度 $O(1)$。
  • d.get(key, default=None)最推荐的访问方式。如果键存在,返回值;如果不存在,返回 default(默认为 None),不会报错
纯文本
plaintext
user = {"name": "Alice", "age": 28}

# 直接访问
print(user["name"])  # 'Alice'
# print(user["email"])  # KeyError: 'email'

# 安全访问
print(user.get("email"))           # None
print(user.get("email", "N/A"))    # 'N/A' (提供默认值)

2. 增加与修改 (Update)

  • d[key] = value:如果 key 存在,则修改其值;如果不存在,则新增键值对。
  • d.setdefault(key, default):如果 key 存在,返回其值;如果不存在,则插入 key: default 并返回 default这是在字典中初始化列表或集合的利器
纯文本
plaintext
user = {"name": "Alice"}

# 修改/新增
user["age"] = 28       # 新增
user["name"] = "Bob"   # 修改

# setdefault 的经典应用:按类别分组
words = ["apple", "bat", "car", "ant", "bear"]
grouped = {}
for word in words:
    first_letter = word[0]
    # 如果 first_letter 不存在,先初始化为空列表,然后 append
    grouped.setdefault(first_letter, []).append(word)

print(grouped)  
# {'a': ['apple', 'ant'], 'b': ['bat', 'bear'], 'c': ['car']}

3. 删除 (Delete)

  • del d[key]:删除指定键值对。键不存在时抛出 KeyError
  • d.pop(key, default):删除指定键并返回其值。如果键不存在且提供了 default,则返回 default;否则抛出 KeyError
  • d.popitem():删除并返回最后插入的键值对(以元组形式)。如果字典为空,抛出 KeyError。(在 Python 3.7 之前,它是随机删除的)。
  • d.clear():清空字典中的所有元素。
纯文本
plaintext
d = {"a": 1, "b": 2, "c": 3}

val = d.pop("b")       # val = 2, d 变为 {'a': 1, 'c': 3}
# d.pop("z")           # KeyError
d.pop("z", "Not Found") # 'Not Found' (安全删除)

last_item = d.popitem() # last_item = ('c', 3), d 变为 {'a': 1}

del d["a"]             # d 变为 {}
d.clear()              # 彻底清空

4. 批量更新:d.update()

将另一个字典或可迭代的键值对合并到当前字典中。已存在的键会被覆盖。

纯文本
plaintext
d1 = {"a": 1, "b": 2}
d2 = {"b": 99, "c": 3}

d1.update(d2)
print(d1)  # {'a': 1, 'b': 99, 'c': 3}

# 也可以使用关键字参数
d1.update(d=4, e=5)

四、 字典的遍历

字典提供了三个视图对象 (View Objects):keys(), values(), items()。它们动态反映字典的变化,且迭代效率极高。

纯文本
plaintext
user = {"name": "Alice", "age": 28, "city": "NY"}

# 1. 遍历键 (默认行为,等同于 user.keys())
for key in user:
    print(key)

# 2. 遍历值
for value in user.values():
    print(value)

# 3. 遍历键值对 (最常用,利用元组解包)
for key, value in user.items():
    print(f"{key}: {value}")

⚠️ 核心避坑:不要在遍历字典时修改其大小
在迭代 keys(), values()items() 时,如果添加或删除了字典的键,会抛出 RuntimeError: dictionary changed size during iteration
正确做法:如果要删除元素,先将其键收集到一个列表中,或者遍历字典的副本 list(d.keys())d.copy()

纯文本
plaintext
d = {"a": 1, "b": 2, "c": 3}

# ❌ 错误做法
# for k in d:
#     if k == "b":
#         del d[k]  # RuntimeError

# ✅ 正确做法 1:遍历键的列表副本
for k in list(d.keys()):
    if k == "b":
        del d[k]

# ✅ 正确做法 2:使用字典推导式创建新字典 (更 Pythonic)
d = {k: v for k, v in d.items() if k != "b"}

五、 字典的进阶特性与现代 Python 语法

1. 字典合并运算符 ||= (Python 3.9+)

这是 Python 3.9 引入的重大语法糖,使得字典合并变得极其优雅,取代了繁琐的 update(){**d1, **d2}

纯文本
plaintext
d1 = {"a": 1, "b": 2}
d2 = {"b": 99, "c": 3}

# | 运算符:返回一个新的合并后的字典 (d2 的值覆盖 d1 的冲突键)
d3 = d1 | d2  
print(d3)  # {'a': 1, 'b': 99, 'c': 3}

# |= 运算符:就地更新 (In-place update),等同于 d1.update(d2)
d1 |= d2
print(d1)  # {'a': 1, 'b': 99, 'c': 3}

2. 字典解包 **

在函数调用或创建新字典时,使用 ** 可以解包字典。

纯文本
plaintext
defaults = {"host": "localhost", "port": 8080}
overrides = {"port": 9000, "timeout": 30}

# 合并字典 (旧版 Python 的优雅写法,Python 3.5+)
config = {**defaults, **overrides}
# 结果: {'host': 'localhost', 'port': 9000, 'timeout': 30}

3. collections.defaultdict (神器)

当需要处理“缺失键”并自动初始化默认值时,defaultdict 比普通字典的 setdefault 更高效、更简洁。它在访问不存在的键时,会自动调用一个工厂函数来生成默认值。

纯文本
plaintext
from collections import defaultdict

# 工厂函数为 list,缺失的键会自动初始化为空列表 []
grouped = defaultdict(list)
words = ["apple", "bat", "ant"]
for word in words:
    grouped[word[0]].append(word)  # 无需检查键是否存在!

print(grouped)  # defaultdict(<class 'list'>, {'a': ['apple', 'ant'], 'b': ['bat']})

# 工厂函数为 int,缺失的键自动初始化为 0 (常用于计数)
counts = defaultdict(int)
for char in "hello":
    counts[char] += 1
print(counts)  # defaultdict(<class 'int'>, {'h': 1, 'e': 1, 'l': 2, 'o': 1})

4. collections.Counter (专用计数器)

Counterdict 的子类,专门用于统计可哈希对象的出现次数,提供了许多便捷方法。

纯文本
plaintext
from collections import Counter

text = "abracadabra"
counter = Counter(text)

print(counter)               # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
print(counter.most_common(2))# [('a', 5), ('b', 2)] (获取出现次数最多的前 2 个元素)

# Counter 支持数学运算
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2)  # Counter({'a': 4, 'b': 3})

六、 字典的底层原理与性能分析 (深度解析)

理解字典的底层实现,是写出高性能 Python 代码的必经之路。CPython 中的字典是基于哈希表 (Hash Table) 实现的。

1. 哈希表的基本工作原理

  1. 计算哈希值:当插入键值对 key: value 时,Python 首先调用 hash(key) 计算出一个整数哈希值。
    to 索引映射:将该哈希值与字典的底层数组大小进行按位与运算(或取模),得到一个数组索引。
  2. 存储:在该索引位置存储 (hash, key, value) 的条目 (Entry)。
  3. 查找:查找时,再次计算 hash(key),直接跳转到对应的索引位置比对 key 是否相等。如果相等,则返回 value

这就是为什么字典的查找、插入和删除操作的平均时间复杂度都是 $O(1)$,远快于列表的 $O(N)$。

2. 哈希冲突与开放寻址法 (Open Addressing)

不同的键可能会计算出相同的哈希值(或映射到相同的索引),这称为哈希冲突。CPython 使用开放寻址法来解决冲突:
如果目标索引已被占用,Python 会使用一个伪随机探测序列(基于哈希值的扰动)去寻找下一个空闲的槽位,直到找到为止。

3. Python 3.6+ 的“紧凑字典”革命 (内存优化) 🌟

在 Python 3.5 及之前,哈希表是一个稀疏数组,为了保持较低的冲突率,数组通常有 1/3 是空的,这导致了严重的内存浪费,且无法保证遍历顺序

从 Python 3.6 (CPython 实现) 开始,并正式纳入 Python 3.7 规范,字典采用了双数组结构

  1. Indices Array (索引数组):一个紧凑的稀疏数组,只存储整数索引。
  2. Entries Array (条目数组):一个稠密数组,按插入顺序依次存储 (hash, key, value)

工作流程
计算 hash(key) -> 映射到 Indices Array 的某个位置 -> 该位置存储的是 Entries Array 中的索引 -> 去 Entries Array 中读取真实数据。

带来的巨大优势

  • 节省内存:条目数组是紧凑的,没有空洞,内存占用减少了 20% 到 25%。
  • 保持插入顺序:因为 Entries Array 是按插入顺序追加的,遍历它自然就保持了顺序。
  • 迭代更快:遍历字典时只需线性扫描紧凑的 Entries Array,无需跳过稀疏的空洞。

4. 字典操作的时间复杂度总结

操作平均时间复杂度最坏情况时间复杂度说明
查找 d[key] / d.get()$O(1)$$O(N)$最坏情况发生在极端哈希冲突时(极罕见)。
插入/修改 d[key] = value$O(1)$$O(N)$均摊 $O(1)$。当哈希表满载时,会触发扩容 (Resize),需要重新计算所有元素的哈希并迁移,此时为 $O(N)$。
删除 del d[key] / pop()$O(1)$$O(N)$删除时会在原位置插入一个特殊的“哑元 (Dummy)”标记,以维持探测序列的完整性。
遍历 d.items()$O(N)$$O(N)$N 为字典中实际存在的元素个数(得益于紧凑数组,非常快)。
键存在性检查 key in d$O(1)$$O(N)$极其高效,这是字典相对于列表的最大优势。

七、 核心避坑指南与最佳实践

1. 键必须是“可哈希的 (Hashable)”

只有不可变对象才是可哈希的。尝试使用列表或字典作为键会导致 TypeError: unhashable type

纯文本
plaintext
# ❌ 错误
bad_dict = {[1, 2]: "value"}  # TypeError

# ✅ 正确:使用元组作为复合键
good_dict = {(1, 2): "value"} 

注意:自定义类的实例默认是可哈希的(基于其内存地址 id)。如果重写了 __eq__ 方法,必须同时重写 __hash__ 方法,否则该实例将变为不可哈希。

2. 判断键是否存在的高效方式

  • 检查键是否存在:始终使用 if key in d:。这是 $O(1)$ 的,且最具可读性。不要使用 if key in d.keys():,虽然 Python 3 中 keys() 视图的 in 也是 $O(1)$,但多此一举。
  • 获取值并处理缺失:优先使用 d.get(key, default),而不是 try...except KeyError,除非缺失键是真正的“异常”情况,或者默认值的计算成本极高(get 会始终计算默认值参数,除非使用 defaultdictsetdefault)。

3. 字典的浅拷贝与深拷贝

与列表类似,字典的 copy() 方法或 d1 = d2.copy() 只是浅拷贝。如果字典的值是可变对象(如列表),修改副本中的列表会影响原字典。

纯文本
plaintext
original = {"a": 1, "b": [1, 2]}
shallow = original.copy()

shallow["b"].append(3)
print(original["b"])  # [1, 2, 3] (原字典被污染)

# ✅ 解决方案:需要完全独立时,使用 copy.deepcopy
import copy
deep = copy.deepcopy(original)

4. 避免在字典中存储过多相同哈希值的键

虽然 Python 的哈希算法(如字符串的哈希)经过了精心设计以抵抗冲突,但如果人为构造大量哈希值相同的键(哈希碰撞攻击),会导致字典操作退化为 $O(N)$。在处理不受信任的外部输入作为字典键时,需保持警惕。


八、 总结

Python 3 的字典不仅是一个简单的键值对容器,它是经过数十年演进的工程杰作。

  • 语法层面:推导式、解包 **、以及 Python 3.9 的 | 合并运算符,使其操作无比流畅。
  • 功能层面defaultdictCounter 等扩展工具,解决了无数常见的数据处理痛点。
  • 底层层面:3.6+ 的紧凑哈希表设计,完美兼顾了极致的内存效率超快的查找速度可预测的遍历顺序

在日常开发中,当你需要快速查找建立映射关系统计计数时,字典永远是第一选择。熟练掌握字典的各种操作模式和底层特性,是区分 Python 新手与资深开发者的关键标志。

如果你对字典在特定场景下的应用(如:实现 LRU 缓存 functools.lru_cache、处理嵌套 JSON 数据、或自定义可哈希类)有进一步的疑问,欢迎随时深入探讨!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注