你天天用的 Python dict,90% 的人没搞懂这三个坑

你天天用的 Python dict,90% 的人没搞懂这三个坑

上周同事的代码在线上崩了,原因是 d['user_id'] 炸了一个 KeyError。我问他为什么不写 d.get('user_id'),他说:"我以为这个 key 肯定在。" 😶

dict 人人会用,但真正理解它底层怎么工作的人没那么多。这篇文章不谈花活,就把最关键的几个底层原理讲清楚。


一、为什么 dict 查找这么快?

1.1 不用 dict 的话,怎么查?

假设要根据名字查成绩,最朴素的做法是用两个列表,靠下标一一对应:

python 复制代码
names  = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]

# 查 Bob 的成绩:先遍历 names 找到索引 1,再 scores[1] 取 75

每次查找都要从头到尾遍历列表。计算机科学里管这叫 O(n)------3 个人最坏找 3 次,10 万个人最坏找 10 万次,数据越多越慢。

1.2 Dict 怎么就不一样?

python 复制代码
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
d['Bob']  # 75,一步到位,不管 d 里有多少条数据

Dict 不靠"遍历",靠**"算"**。就像查字典------你不是从第一页翻到最后一页找"哈希"这个词,而是翻到目录,按拼音 H 一查,直接跳到对应页码。dict 的名字(dictionary)就是这么来的。不管字典有 100 页还是 10000 页,查目录都是一次操作------这就是 O(1),和数据量无关。

1.3 底层原理------哈希表

Dict 的底层是哈希表(Hash Table),核心逻辑是"不靠找,靠算"。

存入数据时:

vbnet 复制代码
key(如 'Bob')→ 哈希函数 → 哈希值(如 284719)→ 内存存储位置 → 放入 value

取数据时,对同样的 key 再算一次哈希,得到同样的位置,直接取走 value。

打个比方:快递柜。哈希函数就像快递柜系统------用你的手机号算出一个柜子编号(比如 38 号)。你取件时不需要把小区所有柜子都打开找一遍,你知道就是 38 号,走过去开门就行。不管是 50 个柜子还是 5000 个柜子,你取一次都是"走到 38 号"这一个动作。

1.4 空间换时间

Dict 有代价------吃内存。哈希表需要预留很多空位来减少"冲突"(两个 key 算到同一个位置,这个话题下次展开)。

Dict(哈希表) List(顺序存储)
查找 O(1),直接算位置 O(n),逐个找
内存 大,预留空位 小,紧凑排列
顺序 无(按哈希值分布) 有(按插入顺序)

一句话:用更多内存换来了瞬间查找。需要高频查数据用 dict,只做遍历用 list。


二、为什么 list 不能当 key?

你一定见过这个报错:

python 复制代码
d = {}
d[[1, 2, 3]] = 'a list'
# TypeError: unhashable type: 'list'

2.1 问题出在"哈希函数要求输入不变"

哈希函数有个铁律:同一个输入,必须永远产出同一个输出。 否则存的时候算出一个位置,取的时候算出另一个位置,数据就丢了。

那什么输入能保证永远不变?

  • 'Bob'------字符串对象创建后就固定了,你没法改它
  • 42------整数永远是 42
  • [1, 2, 3]------但你可以 append、pop、改某个下标的值!

想象你允许列表当 key:

lua 复制代码
1. d[[1,2,3]] = 'hello'  → 对 [1,2,3] 算哈希,存入位置 A
2. key.append(4)          → key 变成了 [1,2,3,4]
3. d[[1,2,3,4]]           → 对 [1,2,3,4] 算哈希,得到位置 B
4. 数据在 A,你在 B 找 → KeyError,而且整个哈希表结构被打乱

这就是为什么 Python 规定:key 必须是可哈希的,也就是不可变类型。

2.2 两派阵营

可当 key(不可变) 不能当 key(可变)
strintfloatbool list(可增删改)
tuple(元素也都不可变才行) dictset

tuple 有个坑:(1, 2, 3) 可哈希,(1, [2, 3]) 不行------因为里面藏了一个 list。


三、Set 就是没有 value 的 dict

Set 和 dict 底层是同一套哈希表实现,唯一区别:dict 存 key + value,set 只存 key。

因为哈希表的 key 不能重复,所以 set 天生去重:

python 复制代码
s = set([1, 2, 3, 2, 5])
print(s)  # {1, 2, 3, 5}  ------ 重复的 2 自动消失

基本操作用 add()remove(),不展开了。

3.1 集合运算------set 真正好用的地方

需求:两个列表找共有元素,再去重合并。

手写循环版:

python 复制代码
a = [1, 2, 3, 2]
b = [2, 3, 4]

# 找共有元素
common = []
for x in a:
    if x in b and x not in common:
        common.append(x)
print(common)  # [2, 3]

# 去重合并
merged = []
for x in a + b:
    if x not in merged:
        merged.append(x)
print(merged)  # [1, 2, 3, 4]

十几行,两层判断,费眼 😮‍💨。

Set 版:

python 复制代码
a = [1, 2, 3, 2]
b = [2, 3, 4]

sa = set(a)  # {1, 2, 3}
sb = set(b)  # {2, 3, 4}

sa & sb  # 交集 → {2, 3}
sa | sb  # 并集 → {1, 2, 3, 4}

& 就是"两个集合都有的",| 就是"合起来去重"------中学数学的集合运算,Python 里两个符号搞定。


四、可变 vs 不可变------sort()replace() 的故事

4.1 猜猜输出

python 复制代码
# 代码 A
a = ['c', 'b', 'a']
print(a.sort())   # ?
print(a)          # ?

# 代码 B
s = 'abc'
print(s.replace('a', 'A'))  # ?
print(s)                    # ?

4.2 答案

python 复制代码
# A
print(a.sort())  # None   ← 为什么不是排序后的列表?
print(a)         # ['a', 'b', 'c']  ← 原列表确实变了

# B
print(s.replace('a', 'A'))  # 'Abc'
print(s)                    # 'abc'  ← 没变!

4.3 关键区分:变量名 vs 对象

这里有一个很容易混淆的点:变量名只是贴在对象上的标签,不是对象本身。

a.sort()可变对象的行为 ------直接改了列表对象自身的内容(把元素重排了),返回 None 是在说"别找我要新对象,我原地改完了"。打个比方:你在白板上擦掉 c b a 重写成 a b c------白板还是那块白板。

str.replace()不可变对象的行为 ------字符串对象一旦创建就不能改,所以 replace 只能创建了一个新字符串 'Abc' 返回给你。原来的 'abc' 纹丝不动。就像你有一张写着 abc 的纸,replace 不是拿涂改液去改------它拿了一张新纸,上面写着 Abc,递给你。

4.4 这张表收好

不可变 (Immutable) 可变 (Mutable)
strintfloatbool listdictset
tuple(元素也都不可变时)

这张表和前面"什么能当 dict key"是完全对应的------不可变的才能哈希,才能当 key。一环扣一环。


总结

四个核心原理:

  1. Dict 快是因为哈希表------不遍历,直接"算位置"。代价是吃内存(空间换时间)
  2. Key 必须是不可变的------哈希函数要求输入永远不变,list 可变所以不配
  3. Set 就是去掉了 value 的 dict ------同一套哈希表,交集 &、并集 | 是核心用法
  4. 可变对象原地改,不可变对象返回新的 ------list.sort() 返回 None 不是 bug,是设计

最后留一个问题:dict 有 1000 万个 key,查询还是 O(1) 吗? 理论上是------但内存不够开始 swap 到硬盘时,情况就变了。哈希冲突与动态扩容,下次聊 😏。


相关推荐
明月_清风1 小时前
TanStack + Cloudflare 边缘实战:从 0 到 1 构建全栈应用
前端·全栈
前端Hardy1 小时前
21.8 万周下载!这个 React 表格组件,10 行代码就能跑起来
前端·javascript·后端
lichenyang4531 小时前
# 鸿蒙 ArkTS 聊天 Demo 功能复盘:真实 SSE、多轮会话、暂停输出、历史记录与防崩溃修复 > 项目:`harmony-chat-demo`
前端
陈_杨1 小时前
鸿蒙APP开发-带你走进胶片录的拍摄记录管理
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进胶片录的相机控制
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进节流战的Canvas图表
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进光绘记的拍摄规划
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进光绘记的长曝光模拟
前端·javascript
陈_杨1 小时前
鸿蒙APP开发-带你走进节拍器的声音怎么这么准
前端·javascript