Python Dict 和 Set 底层原理:从哈希函数到哈希表全方位解析

引言

在 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

数据存入哈希表的流程:

  1. 对数据 Tom 计算哈希值,得到对应数值;
  2. 通过取模运算将哈希值转换为哈希表下标(示例下标为3);
  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) 解决,核心逻辑是「冲突后顺延找空位」。

具体流程:

  1. 数据 Tom 优先占用下标3的槽位;
  2. 数据 Jerry 计算下标同样为3,触发哈希冲突;
  3. 程序自动向后检索下一个下标4,判断槽位是否为空;
  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:

  1. 存入数据时:根据「对象旧内容」计算哈希值,存入指定哈希槽位;
  2. 后续修改对象内容:可变对象哈希值随之改变;
  3. 查询数据时:根据「对象新哈希值」寻址,找不到原本存储的数据;
  4. 最终导致:数据存在但查不到,哈希表结构彻底混乱。

这就是 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:计算哈希值、定位空槽位,完成存储;
  2. 再次插入重复元素 1:计算出完全一致的哈希值,定位到同一槽位;
  3. 程序校验发现槽位已存在相同元素,直接忽略本次插入,最终实现去重效果。

九、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) 的查询、插入和删除效率。

相关推荐
好名字更能让你们记住我1 小时前
【接口自动化测试】博客系统接口自动化测试报告
python·功能测试·自动化·接口测试·接口自动化·测试覆盖率
铁皮哥1 小时前
【后端开发】什么是守护线程,和普通线程有什么区别?
java·开发语言·数据库·人工智能·python·spring·intellij-idea
SilentSamsara1 小时前
FastAPI 实战:从路由定义到依赖注入的完整 REST API
开发语言·python·青少年编程·fastapi
我的xiaodoujiao2 小时前
API 接口自动化测试详细图文教程学习系列23--结合Pytest框架使用4-前后置处理
python·学习·测试工具·pytest
weixin_BYSJ19872 小时前
springboot旅游管理系统04470(附源码+开发文档+部署教程)
java·spring boot·python·算法·django·flask·旅游
kaico20182 小时前
Python 在 Jenkins Pipeline 中的使用总结
开发语言·python·jenkins
多彩电脑2 小时前
在Kivy中制造可移动控件
python
Zy_Yin1232 小时前
拆解如何用anthropic金融agent做投研
人工智能·python·深度学习·金融·github
清水白石0082 小时前
Python 变量的本质:从“盒子思维”到“引用思维”,彻底理解赋值到底发生了什么
java·python·ajax