首页 新闻 会员 周边 捐助

在Python中优化内存使用,关于__slots__的正确说法是:

0
[已解决问题] 解决于 2025-12-04 09:44
A
能完全消除类实例的内存开销
B
允许动态添加新属性到实例
C
通过预置固定属性减少__dict__占用
D
会显著提升实例方法的执行速度
*Tesla*的主页 *Tesla* | 小虾三级 | 园豆:1220
提问于:2025-12-03 14:48
< >
分享
最佳答案
0
  • 普通列表/数组:得遍历所有元素(O(n))
  • 哈希表:通过一个“魔法函数”(哈希函数),把 "name" → 变成一个数字(比如 12345),然后直接去数组的第 12345 个位置取值(O(1))

✨ 这就是“哈希”的魔力:把任意 key 映射到固定范围的整数(数组下标)

CPython 的 dict 使用了一种紧凑型哈希表,不是一张二维表,而是 两个一维数组:

  1. 索引数组(indices):表示“哈希值 → entry 位置”的映射
    • 一个一维数组,每个元素是一个“偏移量”或“entry 编号”
    • 初始大小为 8(即使字典为空)
    • 类型可能是 int8_tint16_tint32_t(根据容量自动选择)
    • indices = [ -1, -1, -1, 2, -1, -1, -1, 1 ] ↑ ↑ ↑ ↑ ↑ 槽0 ... 槽7  (-1是空)
    • 查找过程(以 "name" 为例):

      1. 计算 hash("name") % len(indices) → 假设结果是 2
      2. 查看 indices[2] → 得到 2
      3. 去 entries[2] 找 → key 是 "name",匹配!返回 value "Alice"
  1. 条目数组(entries):按插入顺序存 (key, value)
  2. 冲突怎么办?——开放寻址(Open Addressing)

    如果两个 key 哈希后落到同一个槽(比如 "name""game" 都映射到槽 2),怎么办?

    CPython 使用 探测序列(probing):

    • 如果槽 2 被占了,就试槽 3、槽 4……直到找到空位或匹配项
    • 探测算法是精心设计的(避免聚集)

    这叫 开放寻址法(区别于“链地址法”——用链表存冲突项)。

  3. 空表也占内存,因为要预分配最小容量(通常是 8 个槽):“预留哈希表8个槽位空间(

    避免频繁扩容

    )” 和 “维护散列表结构”。

用 __slots__ 的对象,没有 __dict__ → 不创建哈希表,属性直接存在对象的固定内存偏移处(类似 C struct)

  • dk_indices 初始时所有槽位都是 -1,表示“空”。
  • 当插入一个键(如 "age")时:
    1. 计算 hash("age")
    2. 取模:index = hash("age") % dk_size(这里 dk_size = 8
    3. 如果 dk_indices[index] == -1,就直接用这个槽
    4. 否则,按探测序列找下一个空槽(开放寻址)
    5. 把 entry 的索引号(比如 0)写入该槽

测试扩容:

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?而不是 41611. 数学依据:负载因子 ≤ 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__

典型行业:

  • 游戏开发(实体、粒子)
  • 金融/量化(tick 数据、订单)
  • 大数据处理(日志、事件流)
  • 嵌入式/资源受限环境

 

  • __dict__ 是动态字典:支持任意新增属性(如 p.email = "x@y.com"),但代价是内存大
  • __slots__ 是静态结构:属性在类定义时就固定了,不能动态添加,但内存小、访问快

这不是“换个名字”,而是完全不同的底层存储机制:

  • 一个是哈希表(dict)
  • 一个是连续内存块(类似 C struct)
*Tesla* | 小虾三级 |园豆:1220 | 2025-12-03 17:47
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册