"name" → 变成一个数字(比如 12345),然后直接去数组的第 12345 个位置取值(O(1))✨ 这就是“哈希”的魔力:把任意 key 映射到固定范围的整数(数组下标)
CPython 的 dict 使用了一种紧凑型哈希表,不是一张二维表,而是 两个一维数组:
int8_t, int16_t, int32_t(根据容量自动选择)查找过程(以 "name" 为例):
hash("name") % len(indices) → 假设结果是 2indices[2] → 得到 2entries[2] 找 → key 是 "name",匹配!返回 value "Alice"(key, value)冲突怎么办?——开放寻址(Open Addressing)
如果两个 key 哈希后落到同一个槽(比如 "name" 和 "game" 都映射到槽 2),怎么办?
CPython 使用 探测序列(probing):
这叫 开放寻址法(区别于“链地址法”——用链表存冲突项)。
避免频繁扩容
)” 和 “维护散列表结构”。用 __slots__ 的对象,没有 __dict__ → 不创建哈希表,属性直接存在对象的固定内存偏移处(类似 C struct)
dk_indices 初始时所有槽位都是 -1,表示“空”。"age")时:hash("age")index = hash("age") % dk_size(这里 dk_size = 8)dk_indices[index] == -1,就直接用这个槽测试扩容:
import sys # 测试不同大小 dict 的内存 for n in range(1, 8): d = {i: i for i in range(n)} print(f"n={n}, size={sys.getsizeof(d)}") 输出(典型): Text 编辑 n=1, size=240 n=2, size=240 n=3, size=240 n=4, size=240 n=5, size=240 ← 仍 240! n=6, size=368 ← 扩容了! 根本原因:在“内存浪费”和“频繁扩容”之间找平衡点 哈希表的核心矛盾: 太小 → 插入几个元素就满了 → 频繁 rehash(重建整个表)→ 性能差 太大 → 空表就占很多内存 → 浪费空间 所以必须选一个经验值作为起点。 🔬 二、为什么是 8?而不是 4、16 或 1? 1. 数学依据:负载因子 ≤ 2/3 CPython 要求: 元素数量 ≤ 哈希表容量 × 2/3 所以: 如果初始容量 = 4 → 最多存 floor(4 × 2/3) = 2 个元素 → 插入第 3 个就扩容!太频繁。 如果初始容量 = 8 → 最多存 floor(8 × 2/3) = 5 个元素 → 能容纳大多数“小字典”(如函数局部变量、简单配置等)而不扩容。 如果初始容量 = 16 → 最多存 10 个,但空表内存翻倍(indices 从 32B → 64B+) ✅ 8 是最小的、能容纳“常见小字典”的容量。
为什么说“重”?
| 原因 | 解释 |
|---|---|
| 预留空间 | 空 dict 也分配 8 槽哈希表,为未来插入做准备 |
| 结构复杂 | 需要 indices + entries + 元数据 + 对齐填充 |
| 动态性代价 | 支持任意 key、任意顺序插入、动态扩容 → 必须冗余设计 |
__slots__ 避免创建 __dict__,从而省下这 240 字节/实例
当你需要创建大量结构固定的小对象,且不需要动态属性时,就用
__slots__。
典型行业:
__dict__ 是动态字典:支持任意新增属性(如 p.email = "x@y.com"),但代价是内存大__slots__ 是静态结构:属性在类定义时就固定了,不能动态添加,但内存小、访问快这不是“换个名字”,而是完全不同的底层存储机制: