文章目录

Python __slots__为什么能减少内存占用并加速属性访问

发布于 2026-04-28 01:25:43 · 浏览 7 次 · 评论 0 条

Python 默认的对象创建方式虽然灵活,但在处理数百万甚至上亿个小对象时,会消耗巨大的内存资源并拖慢运行速度。这是由于 Python 默认为每个对象分配了一个字典来存储属性。通过使用 __slots__,我们可以显著优化这两个方面。


1. 理解默认内存开销

在 Python 中,当你定义一个类并实例化时,Python 会自动为每个对象添加一个 __dict__ 属性。这是一个字典,用于存储该对象的所有属性和方法引用。

字典虽然提供了极高的灵活性(允许随时添加新属性),但其内部实现是基于哈希表的,这就带来了额外的内存开销:

  • 哈希表结构:需要维护一个用于存储键值对的数组,以及处理哈希冲突的额外空间。
  • 内存碎片:字典为了减少冲突,通常会预留比实际元素更多的空间。

运行以下代码,查看普通对象的内存占用情况:

import sys

class NormalObject:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 创建实例
obj = NormalObject(10, 20)

# 打印对象自身大小
print(f"对象基础大小: {sys.getsizeof(obj)} 字节")
# 打印内部字典大小
print(f"__dict__ 大小: {sys.getsizeof(obj.__dict__)} 字节")
# 打印总大小
print(f"总大小: {sys.getsizeof(obj) + sys.getsizeof(obj.__dict__)} 字节")

注意:sys.getsizeof 仅计算容器本身,不递归计算容器内的对象。即便如此,你也会发现 __dict__ 占用了相当一部分空间。在创建海量对象时,这种开销会急剧放大。


2. 使用 slots 优化内存

__slots__ 是一个类变量,通过给它赋值一个序列(包含字符串的元组、列表等),你明确告诉 Python 解释器:这个类的实例只包含这些特定的属性。

当定义了 __slots__ 后,Python 会做两件事:

  1. 禁止创建 __dict__
  2. 为每个声明的属性分配固定大小的空间(类似 C 语言的结构体),直接存储在对象实例中。

定义一个使用 __slots__ 的类:

class SlotObject:
    # 限制属性只能是 x 和 y
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

slot_obj = SlotObject(10, 20)

# 打印对象大小
print(f"Slots 对象大小: {sys.getsizeof(slot_obj)} 字节")

# 尝试添加新属性(这将报错,展示了其限制)
try:
    slot_obj.z = 30
except AttributeError as e:
    print(f"错误捕获: {e}")

对比输出结果,你会发现 SlotObject 的实例大小远小于 NormalObject 的总大小。这是因为 Python 不再需要维护那个沉重的哈希表。


3. 内存占用对比分析

为了更直观地理解差异,我们可以创建一个包含大量对象的列表进行对比。

执行以下压力测试代码:

import sys

def create_objects(cls, count):
    return [cls(i, i+1) for i in range(count)]

count = 100000

# 普通类
class A:
    __slots__ = ()
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Slots 类
class B:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 注意:由于 __dict__ 的存在,直接精确对比非常复杂。
# 这里主要观察内存增长趋势的显著差异。
print(f"普通类实例单体参考大小: {sys.getsizeof(A(1,2)) + sys.getsizeof(A(1,2).__dict__)}")
print(f"Slots类实例单体参考大小: {sys.getsizeof(B(1,2))}")

在实际生产环境中,对于百万级实例,使用 __slots__ 通常能节省 30% 到 60% 的内存,具体取决于属性的数量和大小。


4. 加速属性访问的原理

除了节省内存,__slots__ 还能显著加快属性访问速度。这涉及到两种不同的查找机制。

普通对象(字典查找)

访问 obj.x 时,Python 需要:

  1. 找到 obj__dict__
  2. 在哈希表中计算字符串 'x' 的哈希值。
  3. 根据哈希值定位到对应的槽位。
  4. 处理可能发生的哈希冲突。
  5. 返回值。

Slots 对象(描述符查找)

当使用 __slots__ 时,Python 将这些属性转化为了“描述符”。访问过程变为:

  1. Python 知道 x 在内存中的固定偏移量。
  2. 直接通过内存地址访问:$Address = BaseAddress + Offset$。
  3. 返回值。

这是一个从“查表”到“数组索引”的优化,省去了哈希计算和冲突处理的开销。

为了可视化这两种结构的区别,请看以下内存布局图:

graph LR subgraph "普通对象" direction TB Obj1["实例: object_01"] Dict1["__dict__ (哈希表)"] Val1["'x': 100"] Val2["'y': 200"] Obj1 -->|"指针引用"| Dict1 Dict1 --> Val1 Dict1 --> Val2 end subgraph "Slots 对象" direction TB Obj2["实例: object_02"] Arr1["固定数组 (Struct)"] Val3["x: 100"] Val4["y: 200"] Obj2 -->|"内联存储"| Arr1 Arr1 --> Val3 Arr1 --> Val4 end

运行速度测试代码以验证差异:

import timeit

def test_access(cls):
    obj = cls(100, 200)
    # 循环访问属性
    for _ in range(1000000):
        _ = obj.x
        _ = obj.y

# 测试普通类
t_normal = timeit.timeit(lambda: test_access(A), number=10)

# 测试 Slots 类
t_slots = timeit.timeit(lambda: test_access(B), number=10)

print(f"普通类访问耗时: {t_normal:.4f} 秒")
print(f"Slots类访问耗时: {t_slots:.4f} 秒")
print(f"速度提升约: {t_normal/t_slots:.2f} 倍")

5. 掌握使用限制与最佳场景

虽然 __slots__ 性能强大,但它并非没有代价。了解其限制对于正确使用至关重要。

主要限制

  1. 无法动态添加属性:一旦类定义了 __slots__,你将无法给实例添加任何不在列表中的属性。
  2. 类变量冲突__slots__ 定义的名称会与类变量冲突,因为它们描述的是实例数据。
  3. 继承复杂性:如果父类定义了 __slots__,子类也必须定义 __slots__,否则子类实例将拥有父类的 __slots__ 空间以及自己的 __dict__,这反而增加了内存开销。

适用场景对照表

场景特征 是否推荐使用 slots 原因
实例数量极少 优化效果不明显,反而牺牲了灵活性。
纯数据类,字段固定 完美契合,内存节省巨大。
需要动态添加属性 __slots__ 会直接导致 AttributeError。
高性能计算、游戏引擎 减少内存压力,提升数据读写频率。

遵循以下最佳实践步骤:

  1. 识别代码中创建实例数量巨大的类(如粒子系统、股票报价数据)。
  2. 确认这些类的属性在实例生命周期内保持不变。
  3. 定义 __slots__,列出所有属性名称。
  4. 重构所有试图动态添加属性的代码逻辑。

注意:单实例继承时,确保子类也定义了 __slots__

class Parent:
    __slots__ = ('x',)

class Child(Parent):
    # 子类如果不定义 __slots__,通常会回退到带 __dict__ 的模式
    # 正确做法是显式包含父类的 slots (如果需要访问) 或仅定义新 slots
    __slots__ = ('y',)

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文