CPython底层寻址与随机哈希密钥决定set遍历物理顺序

【python】同样的set,不同的输出顺序?深度解析你肯定不知道的set背后的奥秘! — 码农高天

码农高天

Python集合遍历纯粹依循哈希表物理槽位,小整数因自哈希呈现伪有序,而字符串输出乱序则源于解释器防碰撞机制每次启动生成的随机哈希种子。

AI-assisted summary of 【python】同样的set,不同的输出顺序?深度解析你肯定不知道的set背后的奥秘! — original source. Check the original for full context.

Source published:

Key ideas

  • Python set 的输出顺序并非由插入顺序决定,而是完全取决于元素在底层哈希表中的物理槽位排列,遍历时自槽位 0 开始线性向后读取。 (5:00)
  • 小正整数的哈希值即为其本身,计算哈希表槽位时直接映射到低位索引,导致无论正序还是逆序插入都会呈现从小到大的固定遍历顺序。 (8:00)
  • CPython 为防范哈希碰撞拒绝服务攻击,默认在每次启动解释器时随机生成 hash_secret,导致字符串在不同进程运行间的哈希值完全随机,进而使输出顺序不可预测。 (8:00)
  • 通过在终端中设置环境变量 PYTHONHASHSEED=0,可以固定字符串哈希种子,便于在调试时稳定复现由于哈希顺序变化引发的问题。 (8:00)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 2 月 17 日发布。视频源于一位粉丝关于“为什么相同的 set 每次输出顺序都不一样”的提问,作者借此契机深入整理了 Python set 背后的底层数据结构,并系统剖析了关于哈希实现的技术细节。

## TL;DR 视频深入探讨了 Python set 无序且输出不稳定的深层原因。通过对比整数 set 的看似“有序”与字符串 set 的完全随机,高天带领观众查阅 CPython 源码,揭示了 set 基于开放寻址法哈希表的底层实现与线性遍历机制。小整数因哈希值等于自身且无冲突而固定落入对应槽位,而字符串则因 Python 防止哈希碰撞攻击而在每次启动时随机生成哈希种子(Hash Seed),最终导致槽位随机。若需调试与复现,可通过设置环境变量 `PYTHONHASHSEED=0` 来固定随机种子。

## Atmosphere & style 视频风格硬核严谨且通俗易懂,展现出典型资深工程师拨开玄学迷雾的技术探究过程。高天以极具互动感的粉丝提问开篇,通过运行令人反直觉的代码样例制造悬念,随后直击 CPython 源码逐行推导,将晦涩的底层位运算与哈希槽位原理解释得条理清晰、抽丝剥茧。整个观看体验充满求真务实的代码极客氛围,既有顿悟原理的智力快感,又具备很强的工程实用价值。

## Bulletin - [05:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=300s) Python set 的输出顺序并非由插入顺序决定,而是完全取决于元素在底层哈希表中的物理槽位排列,遍历时自槽位 0 开始线性向后读取。 - [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 小正整数的哈希值即为其本身,计算哈希表槽位时直接映射到低位索引,导致无论正序还是逆序插入都会呈现从小到大的固定遍历顺序。 - [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) CPython 为防范哈希碰撞拒绝服务攻击,默认在每次启动解释器时随机生成 hash_secret,导致字符串在不同进程运行间的哈希值完全随机,进而使输出顺序不可预测。 - [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 通过在终端中设置环境变量 PYTHONHASHSEED=0,可以固定字符串哈希种子,便于在调试时稳定复现由于哈希顺序变化引发的问题。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=0s) 与自 Python 3.6 起已实现保序的 dict 不同,Python 的 set 结构始终没有引入记录插入顺序的机制。 - [00:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=0s) 包含连续小整数的 set 输出往往看似经过正向排序,即使倒序插入也同样如此,但这只是低位哈希与槽位对应的巧合而非真实排序。 - [00:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=0s) 当整数范围变化(如 1022 到 1027)时,低位二进制的不同使得元素在哈希表中的映射被打乱,从小到大的假象随即破灭。 - [05:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=300s) CPython 源码中,哈希表的槽位数量必须是 2 的 n 次方,因此掩码 mask 恒等于 slots - 1(二进制全为 1)。 - [05:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=300s) 元素落入哈希表的初始索引通过 hash & mask 快速计算,本质上是取哈希值二进制表示的最后几位作为数组下标。 - [05:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=300s) Set 底层采用开放寻址法(Open Addressing)解决哈希冲突,在冲突时通过步长探测相邻槽位,而非采用链表或红黑树。 - [05:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=300s) Set 输出时调用的 __repr__ 函数依赖迭代器 setiterobject,该迭代器从 table[0] 线性顺序扫描至结尾并输出非空槽位元素。 - [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 对于较小的正整数,其哈希值就等于该数字本身,因此 0 到 4 在未发生碰撞前分别精准占据哈希表的第 0 至第 4 号槽位。 - [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 同一次 Python 运行过程中,相同字符串的哈希值保持恒定;但在多次运行之间,每次生成的 hash_secret 都会改变该值。 - [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 开发者可以通过指定环境变量 PYTHONHASHSEED=0 强制固定随机种子,使得字符串的哈希计算及 set 的遍历顺序跨进程完全一致。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=0s) 粉丝的提问与 set 输出顺序的玄学现象

视频的缘由来自一位 B 站粉丝的提问:大家都知道 Python 的 set(集合)本身是不保序的,但奇怪的是,“对于同样的数据它每一次输出的顺序竟然还不一样呢”。为了说明这个问题,码农高天先梳理了背景:自 Python 3.6 起,字典(dict)就已经实现了保序,即能够记住每个键的插入顺序并在输出时按插入顺序展示,但集合(set)始终没有引入保序机制。

码农高天运行了几组代码来展示这种令人困惑的“玄学”现象: 1. **整数 set(正序插入)**:创建一个包含整数 `[0, 1, 2, 3, 4]` 的 set,连续运行多次,其输出顺序完全固定,不仅没变,而且恰好与传入的列表顺序完全一致。 2. **字符串 set**:将上述数字换成字符串形式放进 set,结果不仅输出乱序,更诡异的是“每一次运行顺序还都不同”。 3. **整数 set(逆序插入)**:创建一个从 `[4, 3, 2, 1, 0]` 逆序插入的 set,输出结果依然是 `0, 1, 2, 3, 4`。这表明它保的根本不是插入顺序,而是呈现出一种“从小到大排列的一个顺序”。 4. **特殊整数范围 set**:传入一组从 1022 到 1027 的整数(`[1022, 1023, 1024, 1025, 1026, 1027]`),其输出顺序并非从小到大排列,规律再次被打破。

这种反直觉的表象背后,唯有通过查阅 CPython 源码才能彻底搞清楚其底层的运作机制。

```mermaid flowchart TD accTitle: Set Insertion Flowchart accDescr: 元素插入 Set 并计算哈希表存储位置的完整源码逻辑

subgraph Input ["元素输入"] A["传入 Python 对象 (key)"] --> B["获取已计算好的哈希值 (hash)"] end

subgraph HashIndex ["计算初始槽位"] B --> C["读取 setobject->mask<br/>(slots - 1, 如 8-1=7, 即 0b111)"] C --> D["按位与运算: i = hash & mask"] end

subgraph TableProbe ["哈希表寻址与冲突处理"] D --> E["定位到 table[i] 槽位"] E --> F{"槽位是否为空?"} F -- "是" --> G["放入当前 entry"] F -- "否" --> H{"哈希相同且对象相等?"} H -- "是" --> I["元素已存在, 忽略"] H -- "否" --> J["线性寻址/探测下一个 entry"] J --> E end ```

---

[05:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=300s) 剖析 CPython 源码:Set 的底层结构与遍历机制

使用列表初始化 set 时,底层实际上是逐一将元素插入 set 中。码农高天深入剖析了 CPython 源码中向 set 添加 entry 的核心函数。该函数接受两个关键参数:`key`(传入的 Python 对象)和 `hash`(在插入前就已经计算出的该对象的哈希值)。

在插入逻辑中,有几个关键步骤: - **`mask` 的含义**:源码注释显示,`mask` 实际上等于 `slots - 1`。由于哈希表的槽位数(slot)必须是 2 的 n 次方,因此 `mask` 是低位由若干个连续的 1 组成的二进制数(如 8 个 slot 时 mask 为 7,即二进制 `111`;256 个 slot 时 mask 为 255,即十六进制 `0xFF`)。 - **槽位索引计算**:`i = hash & mask`。通过按位与操作对哈希值截断,只保留最后几个 bit,将庞大的哈希值转换成一个比 slot 总数小的合法数组下标,这也是哈希表查找的初始位置。 - **冲突处理(Open Addressing)**:如果该槽位为空,元素便直接落入;如果槽位已有数据,则先判断哈希值是否相同,相同再比较对象本身是否一致,一致则说明元素已存在;若不一致则通过探测步长(probes)顺延查找隔壁邻居。码农高天强调,set 的底层并没有采用拉链法(链表)或二叉树,而是标准的开放寻址法,“选择把这个元素放在什么位置完全取决于它的hash甚至于它位置的计算都非常简单就是hash值取后面几个bit 这个数就是它开始式的index值”。

至于 set 打印时的输出逻辑,则是通过其 `__repr__` 函数触发。源码中它把自己转换成一个 list,而这个转换过程依赖迭代器 `setiterobject`。迭代器初始化时会将游标 `si_pos` 设为 0。在调用 `next()` 时,它的逻辑极其简单:从哈希表的 `table[0]` 开始线性向后扫描,检查槽位是否有数据,有数据就弹出一个,接着找下一个槽位。因此,**set 的输出顺序纯粹取决于元素在底层哈希表中的排列物理顺序**。

---

[08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 整数与字符串表现差异的底层根源:Hash 与 Hash Seed

结合源码对哈希表的理解,前面出现的各种整数 set 谜团迎刃而解: - **小整数的哈希特性**:“对于一个比较小的正整数来说 它的hash就是它本身这个事情可能很多人不知道”。对于 0 到 4 来说,它们的哈希值就是 0 到 4,在与 mask(比如 7)进行与运算后,对应的索引分别是 `table[0]`、`table[1]`、`table[2]`、`table[3]`、`table[4]`。由于不存在哈希冲突,无论是正序插入还是倒序插入,它们最终落入哈希表的物理槽位都完全一致,因此打印遍历时自然呈现出从小到大的固定顺序。 - **1022 到 1027 的特殊序列**:初始 set 较小,截取的只是二进制的后几位。1024 恰好是 $2^{10}$,低位全是 0,`1024 & mask` 计算出的索引是 0;1025 的索引是 1;1026 的索引是 2。而 1022 和 1023 的低位为全 1,算出来的是较大的索引,被排在了哈希表的靠后槽位。因此打印顺序依次变成了 `1024, 1025, 1026, 1022, 1023`。

针对字符串 set 每次运行顺序都不同的现象,码农高天指出根源在于字符串哈希的随机化: - 在单次运行中,相同的 Unicode 字符串算出的哈希值必然相同;但如果在多次运行之间哈希结果发生改变,“因为结果不同 一定意味着它的hash不同”。 - CPython 为了防止哈希碰撞拒绝服务攻击,在每次启动 Python 解释器时,都会随机生成一个 `hash_secret`,并使用该密钥参与字符串哈希运算。这导致**同一次运行中相同字符串哈希值不变,但在不同的运行实例之间,相同字符串的哈希值完全随机**。落入的槽位不同,最终遍历输出的顺序自然也就每次都不一样。

如果开发者遇到了因哈希不一致导致的 bug 需要复现,可以通过设置环境变量来固定哈希种子。执行 `PYTHONHASHSEED=0 python example.py` 后,CPython 每次运行都会使用固定的随机种子,此时包含相同字符串的 set 在各次运行中的输出顺序也会完全固定。

- 关键环境变量用法:在终端中指定 `PYTHONHASHSEED=0`,可让每次启动解释器时的字符串哈希计算保持一致。

## Notable quotes > [08:00](https://www.youtube.com/watch?v=MIzt-xTTm8g&t=480s) 对于一个比较小的正整数来说 它的hash就是它本身这个事情可能很多人不知道 — 码农高天

## Mentioned 码农高天, Python, CPython, B站, set, dict, PYTHONHASHSEED

## Video description 这期视频来源于一个粉丝的提问,这个问题很有趣,聊到了set的输出顺序,而答案隐藏的还是比较深的。借这个回答问题的机会,我们可以把set背后的数据结构整理一下,顺便聊一下B站不会有人聊的hash的一些事。

[Source](https://www.youtube.com/watch?v=MIzt-xTTm8g)

中文简报

CPython底层寻址与随机哈希密钥决定set遍历物理顺序 Python集合遍历纯粹依循哈希表物理槽位,小整数因自哈希呈现伪有序,而字符串输出乱序则源于解释器防碰撞机制每次启动生成的随机哈希种子。 视频深入探讨了 Python set 无序且输出不稳定的深层原因。通过对比整数 set 的看似“有序”与字符串 set 的完全随机,高天带领观众查阅 CPython 源码,揭示了 set 基于开放寻址法哈希表的底层实现与线性遍历机制。小整数因哈希值等于自身且无冲突而固定落入对应槽位,而字符串则因 Python 防止哈希碰撞攻击而在每次启动时随机生成哈希种子(Hash Seed),最终导致槽位随机。若需调试与复现,可通过设置环境变量 `PYTHONHASHSEED=0` 来固定随机种子。 Python set 的输出顺序并非由插入顺序决定,而是完全取决于元素在底层哈希表中的物理槽位排列,遍历时自槽位 0 开始线性向后读取。 (5:00) 小正整数的哈希值即为其本身,计算哈希表槽位时直接映射到低位索引,导致无论正序还是逆序插入都会呈现从小到大的固定遍历顺序。 (8:00) CPython 为防范哈希碰撞拒绝服务攻击,默认在每次启动解释器时随机生成 hash_secret,导致字符串在不同进程运行间的哈希值完全随机,进而使输出顺序不可预测。 (8:00) 通过在终端中设置环境变量 PYTHONHASHSEED=0,可以固定字符串哈希种子,便于在调试时稳定复现由于哈希顺序变化引发的问题。 (8:00)

Reading summary

码农高天通过分析 CPython 源码指出,Python 中集合(set)的遍历输出顺序并不具备真正的保序特性,而是完全由元素在底层哈希表中的物理存储槽位决定。整数集合呈现的固定顺序与字符串集合的随机乱序,分别源自小整数的恒等哈希计算以及解释器针对哈希碰撞攻击所采取的随机加盐策略。 底层哈希表结构与线性遍历机制 (5:00) 虽然自 Python 3.6 起字典(dict)已支持记录插入顺序,但集合依然未实现保序。根据 CPython 源码,集合底层采用基于数组的开放寻址法(Open Addressing)解决冲突,而非拉链法或二叉树。哈希表的槽位数始终是 2 的 n 次方,其掩码 `mask` 等于槽位数减一(即二进制低位均为 1)。向集合插入元素时,系统通过 `i = hash & mask` 的按位与操作截取哈希值的低位作为初始下标;若槽位已占用且对象不相等,则通过探测步长向后寻找空位。 集合在通过 `__repr__` 打印输出时,底层迭代器会将游标初始化为 0,并从 `table[0]` 开始对哈希表进行线性扫描。只要遇到非空槽位便依次输出对应元素。因此,集合的输出顺序完全取决于数据在哈希表内存数组中的物理排布顺序,而非元素的插入先后。 整数哈希映射与非递增反例剖析 (8:00) 在针对整数集合的实验中,正序或逆序插入 `0, 1, 2, 3, 4` 均会按从小到大的顺序输出。码农高天解释,较小的正整数其哈希值通常就是其数值本身。在与掩码执行与运算后,0 到 4 分别精确落入索引为 0 至 4 的槽位且无哈希冲突,因此迭代器自左向右扫描必然呈现从小到大的固定顺序。 然而,集合并没有按大小排序的逻辑。当传入 `1022` 至 `1027` 的整数序列时,输出顺序被打乱为 `1024, 1025, 1026, 1022, 1023`。这是因为在初始较小的哈希表中,`1024`(即 $2^{10}$)的二进制低位全为 0,与掩码计算后的索引为 0;而 `1022` 和 `1023` 的二进制低位全为 1,计算得到的槽位靠后,最终在物理扫描时排在 `1024` 之后输出。 字符串哈希随机化与安全性设计 (8:00) 与整数集合不同,包含相同字符串的集合在多次运行同一段程序时,输出顺序往往各不相同。由于单次运行中同一字符串的哈希值固定,输出顺序的改变意味着跨进程运行时的哈希计算结果发生了变化。 码农高天说明,CPython 为了防止针对哈希表的拒绝服务攻击(Hash DoS),在每次启动解释器进程时都会随机生成一个 `hash_secret` 密钥参与字符串哈希运算。这导致相同的字符串在不同运行实例中计算出的哈希值和槽位索引各不相同。若开发者需要复现因哈希顺序不稳定引发的程序缺陷,可在终端运行命令前指定环境变量 `PYTHONHASHSEED=0`,强制解释器使用固定哈希种子以获得确定的输出顺序。 Python 集合的输出表现受制于哈希表槽位计算规则与安全防护机制,小整数集合的“有序性”仅是特殊数值计算碰撞较少的巧合表现;业务代码中若依赖集合的特定输出顺序会导致不可靠的行为,调试相关问题时可通过固定哈希种子消除随机性。

Source quotes

[8:00] 码农高天: 对于一个比较小的正整数来说 它的hash就是它本身这个事情可能很多人不知道

Original transcript

[0:00] 这期视频我们来聊一个很有意思的关于set的问题这个问题来源于我的一个B站粉丝他说 我知道set这个数据结构是不保序的可是为什么对于同样的数据它每一次输出的顺序竟然还不一样呢在聊这个问题之前呢 我们先做一个简单的背景介绍 我们知道python从3.6版本开始字典就已经是宝序的了什么叫宝序呢?就是字典会记住你每一个key的插入顺序并且在输出的时候会按照你的插入顺序输出但是set也就是集合始终就没有这个性质这个事其实很多人是知道的但是让我的粉丝困惑的是下面这件事我们看左边的代码一个是我建立一个里面是integer的set这个Settly是整形01234另一个和上面很相似只不过我把01234变成了一个字符串然后放到了set里我们看一下他们的输出结果可以看到我们运行了几次对于整形的set它永远是宝序的它的顺序不光没变而且就是我给进去的list的顺序而对于字符串,它不光是乱序的,甚至每一次运行顺序还都不同。那这个行为在很多人看来确实有点偏玄学了他很难理解这个现象的背后到底Cpy是怎么工作的在我们去解密之前,不妨给这个问题蒙上一层更神秘的面纱我们刚才说对于整形来说它似乎看起来是宝序的我建立两个set一个是01234另外一个我把它反过来变成43210逆序的set它的输出顺序是什么样的呢大家可以看到神奇的现象发生了它保的并不是插入序而是看起来从小到大排列的一个顺序那对于整形来说它永远会按照从小到大的排列顺序吗我们看我们又加了一组数据是从1022一直到1027很明显它的输出顺序并不是从小到大排列这种现象背后的原因 我们当然只能从cpython的原码里得到解释当我们以一个list作为参数来初始化一个set的时候它实际上就是把list里面的元素逐一插入了set这件事大家如果不相信我的话可以自己去确认一下我在这就不多说了我们把精力集中在这个核心函数上也就是当我往set里面加入一个element的时候或者它这块叫entry的时候它发生了什么首先我们要了解这两个参数这个key就是我加进来这个python object这个hash是这个object的hash也就是在我们尝试添加这个entry之前我们已经把这个object的hash算过了先看这里,mask=setObject的mask这个mask是什么东西我们可以到他定义的地方去读一下他的comment这个mask实际上就是slot-1,而slot又一定是2的n次方所以这个mask就是从第0位开始往左边扩散的若干个1比如说当你有8个slot的时候那这个mask就是7也就是111当你有256个词号的时候这个Mask就是255也就是FF那回到这里这个mask的作用是什么这个mask的作用是把这个hash再进行一次cast把它变成一个合法的slot的index就是我这个hash的值可能很大但是我只取后面几位这样我就能保证在进行完这个操作之后我这个i是比我slot的总数要小的这里涉及一些hash table基础的知识如果你不知道hash table是什么的话自己去温习一下那这个i就是我在hash table上面找的初始的那个位置接下来的124行就可以看到我上来把这个默认的entry设成这个setobjecttable里面的第二个那这个table就是一个array就是一个hash table我把第2个拿出来下面这个probes是我往后试几下这个如果你不理解 现在暂时用不着下面是一个大的do while loop如果发现这个entree是空的我就已经找到应该 [5:00] 把这个object放在哪了如果hash一样,比较这两个object是不是一样?如果他们俩一样的话就说明我这个set里面已经有这个东西了否则的话去找下一个entry那这也是一个非常经典的hash table的实现就是在出现hash conflict的时候怎么办大家比较熟知的可能是在后面建一个link list的或者做一个Binary Tree对吧但这种找隔壁邻居的形式也是非常常见的那简单看下来 这实际上是一个非常直接的hashtable的实现它里面没有任何神奇的地方选择把这个元素放在什么位置完全取决于它的hash甚至于它位置的计算都非常简单就是hash值取后面几个bit 这个数就是它开始式的index值当然了 后面还会有这些找几下找不着应该怎么办的事这件事跟我们今天的主题无关我们就不多说了我们接下来要确认一件事就是它是怎么打印的我们看这里是set的repr函数,也就是它的representation它是把自己变成了一个list而把自己变成一个list的过程你就要去找它的iterator它的Iterator里面首先建立了一个py set iter type的object然后把这个object的si path设成了0我们去看这个setEtherObject他的Ethernet函数也就是在调用next的时候它会产生什么东西这个地方如果你开始听不明白了回去看我生成器的视频Home这个部分其实非常好读就是从这个SI path开始往后找找到有数据的那个entry然后把entry里面那个值拿出来返回那对于set来说就是从table0开始找这里面有没有数据没有就table1再没有table2再没有table3找到一个数据就往处扔一个找到一个数据就往处扔一个所以它的输出顺序是这个值在hashtable里的顺序有了这些背景知识我们就可以返回来看我们的问题了首先对于整形数据这里小小的考一下大家大家知道整形数据的hash是什么吗不知道这个知识算不算冷啊但是一个整形数据它的hash就是它在python里面保存形式的最后若干个bit对于一个比较小的正整数来说 它的hash就是它本身这个事情可能很多人不知道那我们现在看回前两个set就非常明确了无论是我们插入0到4还是4到0由于他们的hash都是没有冲突的所以他们都会被安排到属于自己的那个entry里面并且由于0的hash就是0所以它被放到了table0里 1的hash是1 它被放到了table1里以此类推于是输出的时候就输出了01234看起来就像由小到大叔叔一样那即便插入顺序改变了由于没有出现hash conflict所以他们的位置是没有改变的输出顺序 自然也就是不变的那肯定也有很多人早已经看出来了第三个set是精心设计过的这些数恰好分布在1024的周围这是因为对于初始的set的大小我们只能取到最后几个bit作为index所以1024在跟mast做完end操作之后它剩下的就是01025剩下1 1026剩下2而1022和1023就剩下的是一个比较大的数所以他们被放到了后面于是输出的结果就是1024 1025 1026然后102 1023那怎么解释把这个整形变成一个字符串之后,每一次运行输出的顺序都不一样了呢?很简单,我们只需要搞明白字符串的hash是怎么求出来的就行了我们看这里是字符串hash的代码这行代码就是它求hash的方式对于每一个unicode 它的hash当然是一样的否则的话就不叫hash了 对吧它就违背hash定义了那如果是这样的话 为什么每一次运行的时候它结果都会不同因为结果不同 一定意味着 [10:00] 不同实际上你自己也可以测试一下对于同样的字符创你只可以保证在单次的运行中,它的hash是不变的而在不同次的运行中同样的unicode或者说同样的字符创它的hash是不一样的那如果大家真的非常感兴趣的话它随机的代码在这里在每一次运行python的时候 他会先把hashsecret给做一个随机然后在每一次运行的时候 他会用这个随机产生的hash secret去做一些hash的操作这就导致了同样的字符串在不同次运行的时候它的hash是不一样的而正因为它hash是不同的它在插入hash的时候选择的entry就不一样输出的时候顺序自然也就不一样了那讲到这里一定会有人问我有没有方法让python在每一次运行的时候它的hash行为都是一致的呢比如说我有一些hash导致的bug我希望它可以复现答案是 这是可以的你需要把这个python hashc的environment设置成一个定值我们看当我们用inf python hashc等于0然后再做python example.py的时候它每一次运行的时候包含同样字符串的set 输出顺序也就固定了好 那我们今天的视频就到这里 喜欢的话记得点一下关注 别忘了一键三连哦优优独播剧场——YoYo Television Series Exclusive