引言
在 Python 日常开发中,字典(Dict)和集合(Set)是使用率极高的数据结构:
makefile
# 字典:键值对存储
user = {
"name": "Tom",
"age": 18
}
# 集合:无序元素存储
nums = {1, 2, 3}
我们都熟知它们的核心优势:按键、元素查询的速度极快。
bash
user["name"] # 字典按键取值
1 in nums # 集合元素判断
但绝大多数开发者只知用法、不懂底层,心中常会有这些疑问:
- Dict 凭什么查询、取值速度远超列表?
- Set 为什么可以自动剔除重复元素?
- 为什么 Dict 的 Key 不能是列表、字典这类可变对象?
- 哈希(Hash)到底是什么?它和 Dict、Set 有什么关联?
其实 Python Dict 和 Set 的底层逻辑完全同源,二者所有特性都依托于同一个核心结构:哈希表(Hash Table) 。
完整的底层逻辑链路如下,本文将顺着这条链路层层拆解,帮你建立完整的知识体系:
sql
Dict
↓
Hash Table(哈希表)
↓
Hash Function(哈希函数)
↓
Hash Collision(哈希冲突)
↓
Mutable / Immutable(可变与不可变对象)
↓
Set 的实现原理
一、什么是哈希(Hash)?
哈希的本质是一种不可逆的映射算法 ,可以将任意长度、任意类型的数据,转换为一个固定长度的唯一数字,这个数字就是哈希值(Hash Value) 。
Python 内置 hash() 函数可以直接计算数据的哈希值:
bash
print(hash("Tom"))
输出示例(不同运行环境结果略有差异):
876543210
哈希转换的完整流程:
javascript
"Tom"
↓
Hash Function
↓
876543210
我们可以把哈希值理解为数据的唯一身份证号,不同合法数据会对应专属的哈希标识:
yaml
Tom → 1001
Jerry → 1002
Alice → 1003
依靠这串专属编号,程序可以直接定位数据,无需逐一比对。
二、为什么需要哈希?核心优势:极致高效
想要理解哈希的价值,最好的对比对象是列表(List)。
假设我们用列表存储一组用户名,判断元素是否存在:
bash
users = ["Tom", "Jerry", "Alice"]
print("Tom" in users)
列表的查询逻辑是遍历比对:程序会从第一个元素开始逐一匹配,直到找到目标或遍历完全部元素。
这种查询方式的时间复杂度为 O(n) ,数据量越大,遍历耗时越长,效率越低。
而基于哈希的查询逻辑完全不同:
yaml
Tom
↓
Hash
↓
1001
↓
直接定位
全程无需遍历,直接精准寻址,时间复杂度稳定为 O(1) 。
这就是 Dict、Set 增删查效率碾压列表的核心根本原因。
三、什么是哈希表(Hash Table)?底层存储载体
哈希函数只负责「计算编号」,不负责存储数据,真正承载所有数据、实现高效读写的结构是哈希表。
哈希表的核心本质可以概括为:数组 + 哈希函数,依托数组的连续存储空间,结合哈希算法实现快速寻址。
我们可以简单将哈希表理解为一个带有序下标、预留空槽位的数组:
diff
index
0
1
2
3
4
5
6
7
数据存入哈希表的流程:
- 对数据
Tom计算哈希值,得到对应数值; - 通过取模运算将哈希值转换为哈希表下标(示例下标为3);
- 将数据存入下标为3的槽位中。
存储后结构:
diff
index
0
1
2
3 → Tom
4
5
6
7
数据查询时,只需重复「计算哈希值 → 定位下标 → 读取槽位数据」,全程无遍历,效率极高。
四、Dict 字典的底层实现原理
Python 字典是典型的键值对(Key-Value)哈希表,底层完全基于哈希表实现,所有的按键取值、赋值操作,都是依托哈希寻址完成,稳定保持 O(1) 级别的读写效率。
以用户信息字典为例:
makefile
user = {
"name": "Tom",
"age": 18
}
字典底层的存储单元并非单纯的键值对,而是一组「哈希值+Key+Value」的三元结构,简化示意如下:
css
[ (1234, "name", "Tom"), (5678, "age", 18)]
当我们执行 user["name"] 取值时,底层会严格执行五步逻辑:
步骤1:计算键的哈希值
调用哈希函数,计算 Key 的哈希值:hash("name") = 1234
步骤2:计算存储下标
用哈希值对哈希表总长度取模,得到数据对应的槽位下标:1234 % 表长度 = 目标下标
步骤3:定位存储槽位
根据计算出的下标,直接锁定哈希表中的对应槽位。
步骤4:校验 Key 一致性
对比槽位中存储的 Key 与查询 Key 是否完全一致(用于规避哈希冲突误差)。
步骤5:返回目标 Value
校验通过后,直接取出对应的 Value 值,完成查询。
整个过程无任何遍历操作,这也是字典查询极致高效的核心逻辑。
五、哈希表的核心难题:哈希冲突
哈希算法存在一个无法完全避免的问题:哈希冲突(Hash Collision) 。
简单来说:不同的数据,经过哈希计算后,得到了相同的哈希下标。
举个典型示例:
Tom
↓
Hash
↓
3
Jerry
↓
Hash
↓
3
两个不同的数据,需要存入同一个槽位,就产生了哈希冲突,若不解决会导致数据覆盖、丢失。
六、Python 解决哈希冲突的方案:开放寻址法
Python 针对 Dict、Set 的哈希冲突,统一采用 开放寻址法(Open Addressing) 解决,核心逻辑是「冲突后顺延找空位」。
具体流程:
- 数据
Tom优先占用下标3的槽位; - 数据
Jerry计算下标同样为3,触发哈希冲突; - 程序自动向后检索下一个下标4,判断槽位是否为空;
- 若下标4为空,则将
Jerry存入该槽位;若已被占用,继续顺延检索下标5、6......直至找到空槽位。
解决冲突后的最终存储结构:
0
1
2
3 → Tom
4 → Jerry
5
6
7
通过这种方式,Python 完美规避了哈希冲突导致的数据异常,保证哈希表数据完整性。
七、核心面试考点:为什么 Dict 的 Key 必须是不可变对象?
这是 Python 高频面试题,核心答案只有一句话:保证哈希值的稳定性。
想要彻底弄懂「为什么 Dict 的 Key、Set 的元素必须是不可变对象」,必须先搞懂 Python 中可变对象(Mutable) 和不可变对象(Immutable) 的核心区别,这是哈希表存储规则的底层前提。
7.1 什么是不可变对象?
不可变对象 :对象创建完成后,内存中的数据内容无法被修改。如果对变量进行赋值、修改操作,不会改动原内存数据,而是会开辟新内存、生成一个全新的对象。
Python 中典型不可变对象:int、float、bool、str、tuple
核心特性:哈希值永久固定
因为内容无法修改,所以其计算出来的哈希值永远不会改变,具备稳定的哈希特性,这也是它能作为 Dict Key、Set 元素的核心原因。
以字符串(不可变对象)为例演示:
bash
a = "Tom"
print(hash(a))
# 重新赋值,不是修改原对象,而是生成新对象
a = "Jerry"
print(hash(a))
原字符串 "Tom" 的哈希值始终固定,不会被任何操作修改。
7.2 什么是可变对象?
可变对象 :对象创建后,可以直接修改内存中的原始数据,不会开辟新内存,对象本身始终是同一个。
Python 中典型可变对象:list、dict、set
核心特性:哈希值不稳定、不可哈希
因为内容可以随时被修改,对象的哈希值会跟随内容变化,哈希值不固定,因此不支持哈希运算 ,属于unhashable类型。
以列表(可变对象)为例演示:
bash
lst = [1, 2, 3]
# 直接修改原内存数据
lst.append(4)
# 列表内容改变,哈希值会发生变化
print(hash(lst)) # 直接报错
报错原因:可变对象动态可变,Python 不允许对其计算哈希值。
7.3 可变/不可变对象核心区别对照表
| 特性 | 不可变对象(Immutable) | 可变对象(Mutable) |
|---|---|---|
| 内存数据 | 创建后不可修改 | 可直接原地修改 |
| 哈希值 | 固定、稳定、可哈希 | 动态变化、不可哈希 |
| 能否作为 Dict Key | ✅ 可以 | ❌ 不可以 |
| 能否作为 Set 元素 | ✅ 可以 | ❌ 不可以 |
| 常见类型 | int、str、float、bool、tuple | list、dict、set |
7.4 为什么哈希结构拒绝可变对象?
结合前面的哈希表原理,我们可以彻底闭环这个知识点:
Dict 和 Set 的所有存储、查询、去重逻辑,全部依赖哈希值定位槽位。
如果使用可变对象作为 Key / 元素,会出现致命 bug:
- 存入数据时:根据「对象旧内容」计算哈希值,存入指定哈希槽位;
- 后续修改对象内容:可变对象哈希值随之改变;
- 查询数据时:根据「对象新哈希值」寻址,找不到原本存储的数据;
- 最终导致:数据存在但查不到,哈希表结构彻底混乱。
这就是 Python 强制规则的底层根源:只有哈希值永久稳定的不可变对象,才能参与哈希表存储。
若使用可变对象作为字典 Key,会直接抛出类型错误:
ini
# 非法用法
d = {[1, 2]: "hello"}
报错信息:
bash
TypeError: unhashable type: 'list'
因此,只有哈希值固定的不可变对象,才能作为 Dict 的 Key、Set 的元素。
八、Set 集合的核心特性与去重原理
Set(集合)是 Python 无序、无重复的哈希结构,核心特性:元素唯一、无序存储、查询速度快。
最直观的特性就是自动去重:
ini
nums = {1, 2, 3, 3, 3}
print(nums) # 输出:{1, 2, 3}
Set 自动去重的核心原理,依旧依托哈希表:
- 首次插入元素
1:计算哈希值、定位空槽位,完成存储; - 再次插入重复元素
1:计算出完全一致的哈希值,定位到同一槽位; - 程序校验发现槽位已存在相同元素,直接忽略本次插入,最终实现去重效果。
九、Set 的底层实现:与 Dict 同源复用
很多人误以为 Set 是独立的数据结构,实则不然。Set 和 Dict 底层共用同一套哈希表逻辑,只是存储形式不同。
二者核心区别:
- Dict(字典) :哈希表存储
Key + Value键值对; - Set(集合) :哈希表仅存储
Key,无 Value 概念。
我们可以通俗理解:Set 就是所有 Value 统一为 None 的字典。
{1, 2, 3} 底层等价于:
python
{1: None, 2: None, 3: None}
精简总结:
- Dict = 存储 Key+Value 的哈希表
- Set = 仅存储 Key 的哈希表
ini
Dict = HashTable(Key + Value)
Set = HashTable(Key)
十、Dict 与 Set 完整关联关系
二者同源同底层,依托同一套哈希体系实现所有功能,关联关系如下:
哈希函数 → 生成哈希值 → 驱动哈希表运行
哈希表衍生出两大结构:
- Dict(字典) :存储 Key+Value,支持 O(1) 增删查改,用于键值对数据存储;
- Set(集合) :仅存储 Key,支持 O(1) 增删查,用于去重、成员判断、集合运算。
scss
Hash Function
│
▼
Hash Table
/ \
/ \
▼ ▼
Dict Set
Key+Value Key
O(1)查询 O(1)查询
O(1)插入 O(1)插入
O(1)删除 O(1)删除
全文总结
Dict 和 Set 的所有特性都不是"语法魔法",全部源于哈希表的底层设计,完整逻辑链路闭环:
sql
数据
↓
Hash Function
↓
Hash Value
↓
Hash Table
↓
Dict / Set
其中:
- Hash 负责将数据映射为数字。
- Hash Table 负责根据哈希值快速存储和查找数据。
- Dict 与 Set:底层同源,区别仅在于是否存储 Value,Set 本质是无值的字典。
- 哈希冲突:不同数据哈希下标重合,Python 通过开放寻址法解决
- 不可变对象 由于哈希值稳定,可以作为 Dict Key 和 Set 元素。
- 可变对象 哈希值可能变化,因此不能参与哈希表存储。
正因为有这套机制,Python 的 Dict 和 Set 才能实现接近 O(1) 的查询、插入和删除效率。