文章目录

Python __slots__属性对类内存占用与属性访问的影响

发布于 2026-04-19 23:17:46 · 浏览 8 次 · 评论 0 条

Python 默认的类实例机制通过字典 __dict__ 存储属性,虽然灵活,但会消耗大量内存。在需要创建成千上万个实例的场景下(如游戏角色、传感器数据点),这种内存开销会变得难以承受。使用 __slots__ 属性可以显著降低内存占用并提升属性访问速度。

以下是指南正文:


1. 理解默认机制的内存开销

Python 的类默认包含一个 __dict__ 字典,用于动态存储实例属性。字典基于哈希表实现,为了保证查询效率和解决哈希冲突,它会预留额外的内存空间。

查看默认类的内存占用:

class RegularClass:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 实例化对象
obj = RegularClass("Alice", 30)

# 打印内存占用 (单位:字节)
print(f"默认类实例大小: {obj.__sizeof__()} 字节")

# 打印底层字典大小
print(f"内部字典大小: {obj.__dict__.__sizeof__()} 字节")

输出结果表明,对象本身的指针很小,但其内部的 __dict__ 占用了大量空间(通常在 280 字节以上,取决于 Python 版本和操作系统),且随着属性数量增加,哈希冲突会导致内存占用呈非线性增长。


2. 使用 __slots__ 优化内存

__slots__ 是一个类变量,通过将有效属性名列表赋值给它,Python 解释器会预先分配固定大小的内存空间来存储这些属性,而不是使用动态字典。

定义一个带有 __slots__ 的类:

class SlotClass:
    # 显式声明允许的属性名
    __slots__ = ['name', 'age']

    def __init__(self, name, age):
        self.name = name
        self.age = age

# 实例化对象
slot_obj = SlotClass("Bob", 25)

# 打印内存占用
print(f"Slot类实例大小: {slot_obj.__sizeof__()} 字节")

对比两者的内存差异:

创建两个列表,分别包含 100,000 个实例,查看总内存占用。

import sys

# 创建列表
regular_list = [RegularClass(f"User{i}", i) for i in range(100000)]
slot_list = [SlotClass(f"User{i}", i) for i in range(100000)]

# 计算总大小 (粗略估算,实际包含循环引用等开销,但足以反映数量级差异)
regular_size = sum(sys.getsizeof(o) + sys.getsizeof(o.__dict__) for o in regular_list)
slot_size = sum(sys.getsizeof(o) for o in slot_list)

print(f"默认类总内存: {regular_size / 1024 / 1024:.2f} MB")
print(f"Slot类总内存: {slot_size / 1024 / 1024:.2f} MB")
print(f"内存节省比例: {(1 - slot_size / regular_size) * 100:.1f}%")

典型结果如下表所示(具体数值视环境而定):

类类型 单实例占用空间 100,000 实例总占用 特点
默认类 (含 __dict__) ~332 字节 ~32 MB 属性可动态添加,内存占用大
__slots__ ~56 字节 ~5 MB 属性固定,内存占用极低

通过表格数据可以看出,对于属性简单的类,使用 __slots__ 可以节省 60% 到 80% 的内存。


3. 属性访问速度的影响

除了节省内存,__slots__ 还能提升属性访问速度。默认的属性访问需要经过哈希查找,而 __slots__ 通过描述符协议直接访问内存中的偏移量,这类似于 C 语言的结构体,速度更快。

执行速度测试代码:

import timeit

def test_regular_access():
    obj = RegularClass("Test", 0)
    for _ in range(1000000):
        _ = obj.name
        obj.age = 20

def test_slot_access():
    obj = SlotClass("Test", 0)
    for _ in range(1000000):
        _ = obj.name
        obj.age = 20

# 计时
regular_time = timeit.timeit(test_regular_access, number=100)
slot_time = timeit.timeit(test_slot_access, number=100)

print(f"默认类平均耗时: {regular_time:.5f} 秒")
print(f"Slot类平均耗时: {slot_time:.5f} 秒")

通常情况下,SlotClass 的访问速度会比 RegularClass20% 到 40%。在高频调用的性能关键路径(如物理引擎、数值计算)中,这种优化非常可观。


4. 决策流程:何时使用 __slots__

虽然 __slots__ 有明显优势,但它牺牲了灵活性,且增加了继承的复杂度。请参照以下流程图决定是否使用:

graph TD A[开始: 定义新类] --> B{需要创建大量实例?
如数千或数万个} B -- 否 --> C[不使用 __slots__
保持默认灵活性] B -- 是 --> D{属性是否固定?
即运行期是否动态添加属性} D -- 否, 需动态添加 --> C D -- 是, 属性固定 --> E{是否需要多重继承?
且父类已定义 __slots__} E -- 是, 关系复杂 --> F[谨慎评估:
需在当前类合并父类 slots] E -- 否 / 单继承 --> G[使用 __slots__] F --> G G --> H[享受低内存与高访问速度]

5. 严格遵守的限制清单

在决定使用 __slots__ 后,检查以下代码陷阱,避免程序报错。

  1. 禁止动态添加未声明的属性
    一旦定义了 __slots__,实例将不再拥有 __dict__

    obj = SlotClass("Test", 20)
    obj.new_attr = "New"  # 这行会抛出 AttributeError: 'SlotClass' object has no attribute 'new_attr'
  2. 类变量需要单独处理
    __slots__ 仅对实例属性生效。如果需要类级别的共享变量,定义在类级别,而非放入 __slots__ 列表。

    class SlotClass:
        __slots__ = ['name'] # 只管实例属性 name
        shared_var = 0      # 类变量,不受影响
  3. 继承时的名称冲突
    如果子类没有定义 __slots__,它会拥有父类的 __slots__ 加上自己的 __dict__。如果子类定义了 __slots__,它只会拥有自己声明的属性。确保子类的 __slots__ 包含所有需要用到的父类属性名(或者在父类中预留),否则无法访问父类属性。

    class Parent:
        __slots__ = ['x']
    
    class Child(Parent):
        __slots__ = ['y'] 
        # 这里的实例能访问 x (继承) 和 y
        # 但如果 __slots__ = [],则无法访问 x

评论 (0)

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

扫一扫,手机查看

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