Python dict 的哈希实现与冲突处理

6次阅读

python字典哈希值通过hash(key)扰动后与(table_size-1)按位与得到索引,要求表大小为2的幂;冲突时采用伪随机探测,删除打标记不释放内存,扩容阈值0.65触发翻倍重建。

Python dict 的哈希实现与冲突处理

Python 字典的哈希值是怎么算出来的

Python 的 dict 底层靠哈希表实现,每个键(key)必须是可哈希的(即有 hash 方法且不可变)。哈希值不是直接用 hash(key) 原样存进去的,而是做了位运算扰动:
hash(key) & (table_size - 1) —— 这要求表大小始终是 2 的幂,所以实际索引是哈希值的低位截取。

  • 如果自定义类作为 key,必须同时实现 hasheq,否则可能查不到或重复插入
  • strinttuple(只含可哈希元素)天然支持,但 listdictset 不行,会报 TypeError: unhashable type
  • 同一进程内,相同字符串hash() 值稳定;但 Python 3.3+ 默认启用哈希随机化(-RPYTHONHASHSEED),跨进程不保证一致

哈希冲突时 Python 怎么找下一个空槽

冲突发生时,CPython 用开放寻址法(open addressing),具体是 伪随机探测(perturb-based probing),不是线性探测或二次探测。

  • 探测序列由初始哈希和一个不断右移的扰动值 perturb 共同决定:i = (5*i) + 1 + perturb; perturb >>= 5
  • 这个设计让冲突分布更均匀,避免“聚集”(clustering)
  • 空槽(NULL)、已删除槽(DELETED)、有效槽(USED)三态并存,删除操作不真正清内存,只打标记,避免查找中断

常见错误现象:

  • 插入大量键后字典变慢,不一定是哈希差,更可能是负载因子超过 0.65 触发扩容(复制整个哈希表)
  • 频繁增删导致大量 DELETED 槽,虽不占逻辑空间,但影响探测效率,最终也会触发重建

为什么有时两个不同对象 hash() 相同却能共存于 dict

哈希值相同 ≠ 键相等。Python 查找时分两步:

立即学习Python免费学习笔记(深入)”;

  1. 计算哈希值,定位大致桶位置
  2. 在该桶及后续探测路径中,逐个比对 key is other or key == other(先用 is 快判,再用 ==

所以:

  • hash(1)hash(1.0) 相同,但 1 == 1.0True,它们不能同时存在:后者会覆盖前者
  • hash("a")hash("b") 可能碰巧相同(尤其小字符串),只要 "a" != "b",就能共存,只是查找要多探几次
  • 自定义类若 hash 返回常数(如 return 42),所有实例哈希都一样,性能退化为 O(n),但语法上完全合法

扩容机制如何影响性能和内存占用

字典在插入时检查负载因子(used / table_size),超过 0.65 就扩容(翻倍),但扩容不是简单复制:

  • 新表大小是 ≥ 2×旧容量的最小 2 的幂(例如从 8 → 16,但 128 → 256)
  • 所有键值对按新哈希重新散列,旧探测路径作废
  • 删除操作不触发缩容,只有显式调用 dict.clear() 或引用被回收才释放内存
  • 使用 dict.fromkeys(keys, default) 初始化大批量数据时,如果 keys 是 list,内部仍会逐个插入,无法预分配最优大小;想省时间得先估算数量,再用 {k: default for k in keys}(解释器有优化)

容易被忽略的一点:哈希表的「空闲槽」其实一直存在,哪怕你删光所有项,只要没触发 gc 或重新赋值,底层数组不会缩小。观察 sys.getsizeof(d) 就能发现这点。

text=ZqhQzanResources