字典(dict)是Python中使用频率最高的数据结构之一,它以键值对的形式组织数据,查找效率极高。很多初学者会用字典,却不太清楚它背后的存储机制,比如为什么字典的键不能是列表、为什么遍历字典的顺序和插入顺序一致。理解字典的底层实现,能帮助你在写代码时做出更合理的选择,也能在遇到性能问题时快速定位原因。

字典的底层实现:哈希表是如何工作的
Python字典的核心是一张哈希表。当你执行d[key] = value时,Python会先对key调用hash()函数,得到一个哈希值,再根据这个哈希值计算出该键值对在内部数组中的存储位置。查找时同样走这条路径:先算哈希值,再定位索引,最后比较键是否相等。整个过程不需要遍历整个字典,时间复杂度接近O(1),这就是字典查找快的原因。
既然不同对象的哈希值可能相同,也就是出现哈希冲突,Python是怎么处理的呢?早期版本的CPython使用开放定址法,冲突时按探测序列寻找下一个空位。Python 3.6之后,字典的实现做了重大改进,引入了紧凑字典(compact dict)的结构:一张稀疏的索引表加一张密集的键值对数组。索引表只存整数下标,键值对按插入顺序紧凑排列。这种设计既节省了内存,又让字典天然保持插入顺序,这也是从Python 3.7开始官方保证字典有序的原因。
理解了哈希机制,也就明白了为什么键必须是不可变类型。列表是可变的,如果允许它做键,修改列表内容后其哈希值会与存储时不一致,就再也找不到原来的键值对了。所以字符串、数字、元组(元组内也不能含可变元素)都可以做键,而列表和字典不行。可以动手验证一下:
d = {}
d[([1, 2])] = "test" # 会抛出 TypeError: unhashable type: 'list'
# 元组是不可变的,可以作为键
point = {}
point[(3, 5)] = "坐标A"
print(point[(3, 5)]) # 输出:坐标A
字典的创建与常用读写方法
创建字典有多种方式,最常见的是花括号语法和dict()构造函数。如果要根据两个序列快速组装字典,zip()配合dict()非常方便。还有一种字典推导式,适合从已有数据中筛选生成新字典。
# 三种常见创建方式
d1 = {"name": "张三", "age": 25}
d2 = dict(name="李四", age=30)
d3 = dict(zip(["a", "b", "c"], [1, 2, 3]))
# 字典推导式:筛选出成绩及格的学生
scores = {"小明": 88, "小红": 45, "小刚": 92}
passed = {name: score for name, score in scores.items() if score >= 60}
print(passed) # 输出:{'小明': 88, '小刚': 92}
读取字典时,直接用中括号d[key]是最快的,但键不存在时会抛出KeyError。get()方法则更安全,可以在键缺失时返回默认值而不报错,这在处理配置项、统计词频等场景特别实用。
config = {"host": "127.0.0.1", "port": 8080}
# 中括号访问,键不存在会报错
print(config["host"])
# get方法访问,键不存在返回默认值
print(config.get("timeout", 30)) # 输出:30
# 统计词频的经典写法
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counter = {}
for w in words:
counter[w] = counter.get(w, 0) + 1
print(counter) # 输出:{'apple': 3, 'banana': 2, 'cherry': 1}
新增和修改比较简单,直接赋值即可,键存在就是修改,不存在就是新增。setdefault()方法适合处理嵌套结构:如果键不存在就先设置默认值再返回,存在则直接返回已有值,常用于构建多级字典。
data = {}
data.setdefault("users", []).append("张三")
data.setdefault("users", []).append("李四")
print(data) # 输出:{'users': ['张三', '李四']}
# 对比直接赋值的繁琐写法
if "logs" not in data:
data["logs"] = []
data["logs"].append("系统启动")
删除、合并与遍历技巧
删除元素有几种方式。pop()删除指定键并返回对应的值,键不存在时可给默认值避免报错;popitem()删除并返回最后插入的键值对,配合循环可以清空字典;del语句适合直接删除确定存在的键;clear()则一次性清空整个字典。
user = {"name": "王五", "age": 28, "city": "北京"}
name = user.pop("name") # 删除并返回值
age = user.pop("age")
last = user.popitem() # 删除并返回最后一个键值对
del user["city"] # 直接删除
user.clear() # 清空字典
合并字典时,update()方法会用另一个字典的内容覆盖当前字典的同名键,常用于配置合并。从Python 3.5起还可以用{**d1, **d2}解包语法,Python 3.9之后更是支持|和|=运算符,写法越来越简洁。
default_cfg = {"host": "127.0.0.1", "port": 8080, "debug": False}
custom_cfg = {"port": 9090, "debug": True}
# 方式一:update,会修改原字典
merged = default_cfg.copy()
merged.update(custom_cfg)
# 方式二:解包语法,生成新字典
merged = {**default_cfg, **custom_cfg}
# 方式三:Python 3.9+ 合并运算符
merged = default_cfg | custom_cfg
print(merged) # 输出:{'host': '127.0.0.1', 'port': 9090, 'debug': True}
遍历字典时,keys()、values()、items()三个方法分别返回键、值和键值对视图。视图对象是动态的,遍历过程中修改字典会导致RuntimeError,这一点要特别注意。如果确实需要在遍历时删除元素,可以先用list()把键固定下来。
scores = {"小明": 88, "小红": 45, "小刚": 92}
# 遍历键值对
for name, score in scores.items():
print(f"{name}的分数是{score}")
# 遍历时删除不及格的学生,需要先转成列表
for name in list(scores.keys()):
if scores[name] < 60:
del scores[name]
print(scores) # 输出:{'小明': 88, '小刚': 92}
此外还有一些细节值得留意:判断键是否存在用in运算符而不是get(),语义更清晰;fromkeys()可以快速生成所有值相同的字典;处理深层嵌套字典时,标准库的collections.defaultdict比setdefault()更灵活,ChainMap则适合多个字典串联查询的场景。掌握这些方法后,字典基本可以应对日常开发中绝大多数的数据组织需求。