在 Python 中,我们习惯了对象的动态特性 —— 可以随时给实例添加新的属性。这非常灵活,但在处理大量数据对象时,这种灵活性会带来不小的内存开销。__slots__正是为了解决这个问题而生的强大工具。
本教程将带你全面了解__slots__的功能、基本用法以及在继承场景下的注意事项,所有代码示例均经过实际运行验证,确保结果真实可靠。
1. 什么是__slots__?
__slots__是 Python 类中的一个特殊类属性(class attribute)。
默认情况下,Python 类的实例会有一个__dict__属性,这是一个字典,用来存储实例的所有属性。这个字典允许我们在运行时动态地添加新属性,但它本身也会消耗大量的内存。
当你在类中定义了__slots__时,Python 会:
- 为声明的属性在内存中预留固定的空间。
- 不再为每个实例自动创建
__dict__和__weakref__。 - 限制实例只能拥有
__slots__中声明的那些属性。
这意味着,通过牺牲一点点动态性,我们换来了巨大的内存节省和一定的访问速度提升。
2. 核心功能
2.1 显著的内存优化
这是使用__slots__最主要的原因。对于拥有成千上万个实例的数据类来说,节省的内存是非常可观的。
普通的 Python 对象,每个实例的__dict__通常需要几百字节的开销,而使用了__slots__的实例,每个属性只占用固定的字段大小,类似于 C 语言中的结构体。
可运行的内存对比代码
以下测试结果基于Python 3.10.12 + Ubuntu 22.04环境。
不同的 Python 版本或操作系统,结果可能会有差异(例如 Python 3.11+ 对普通对象内存有额外优化),请以你本地运行的实际结果为准。
你可以直接运行下面这段代码,在你的环境中亲眼看看两者的差距(使用标准库tracemalloc精确测量):
importtracemalloc# 1. 普通类classNormalPoint:def__init__(self,x,y):self.x=x self.y=y# 2. 使用 __slots__ 的类classSlotPoint:__slots__=('x','y')def__init__(self,x,y):self.x=x self.y=y# 测试创建 10 万个实例的内存占用deftest_memory(cls,count=100000):tracemalloc.start()instances=[cls(i,i+1)foriinrange(count)]snapshot=tracemalloc.take_snapshot()top_stats=snapshot.statistics('lineno')total=sum(stat.sizeforstatintop_stats)tracemalloc.stop()returntotal normal_mem=test_memory(NormalPoint)slot_mem=test_memory(SlotPoint)print(f"创建10万个普通实例,总内存:{normal_mem/1024:.2f}KB")print(f"创建10万个Slots实例,总内存:{slot_mem/1024:.2f}KB")print(f"节省了:{(normal_mem-slot_mem)/1024:.2f}KB 内存")我们环境下的实际运行结果:
创建10万个普通实例,总内存: 21081.03 KB 创建10万个Slots实例,总内存: 10924.84 KB 节省了: 10156.19 KB 内存仅仅 10 万个实例,就节省了近 10MB 的内存!当你实例化更多对象时,差距会更加惊人。
2.2 严格的属性限制
定义了__slots__后,你就不能再给实例添加__slots__中未声明的属性了。这可以帮助你:
- 防止拼写错误:如果不小心打错了属性名,Python 会直接抛出
AttributeError,而不是静默地创建一个新的、无用的属性。 - 强制接口规范:确保类的使用者不会随意修改对象结构,让代码更加健壮。
2.3 更快的属性访问
由于属性不再是通过字典的哈希表查找,而是通过固定的偏移量直接访问内存,这使得属性的读写速度会略快于普通对象。虽然对于少量对象来说提升不明显,但在高性能场景下依然有帮助。
3. 基本使用
3.1 基础语法
使用__slots__非常简单,只需要在类定义中添加一个名为__slots__的类变量即可。它通常是一个字符串序列(tuple 或 list),列出你允许实例拥有的所有属性名。
classPoint:# 声明允许的属性__slots__=('x','y')def__init__(self,x,y):self.x=x self.y=y# 正常使用p=Point(10,20)print(p.x)# 输出: 10print(p.y)# 输出: 203.2 动态属性被禁止
尝试添加一个未声明的属性会发生什么?
# 尝试添加新属性 zp.z=30运行结果:
AttributeError: 'Point' object has no attribute 'z'这正是我们想要的!它阻止了意外的属性创建。对比一下普通类的行为:
classNormalPoint:def__init__(self,x,y):self.x=x self.y=y n_p=NormalPoint(10,20)n_p.z=30# 这在普通类中是允许的!print(n_p.__dict__)# 输出: {'x': 10, 'y': 20, 'z': 30}# (拼写错误在这里很难被发现)3.3 没有__dict__
定义了__slots__的实例没有__dict__:
print(hasattr(p,'__dict__'))# 输出: False4. 继承中的注意事项
__slots__的行为在继承中会变得稍微复杂一些,这也是最容易出错的地方。根据 Python 官方文档,我们需要注意以下几点:
4.1 单继承:子类是否定义__slots__?
父类定义的__slots__会自动继承给子类。但是,子类是否自己定义__slots__会导致完全不同的结果。
情况 1:子类没有定义自己的__slots__
如果父类有__slots__,但子类没有,那么子类的实例仍然会拥有__dict__!
这意味着,虽然子类继承了父类的 slots,但你依然可以给子类实例动态添加新属性,因为它有字典。这也意味着,你失去了大部分的内存优化效果。
classParent:__slots__=('x','y')classChild(Parent):# 注意:这里没有定义 __slots__passc=Child()c.x=10c.y=20c.z=30# 这居然是允许的!因为 Child 有 __dict__print(hasattr(c,'__dict__'))# 输出: Trueprint(c.__dict__)# 输出: {'z': 30}如果你想让子类也保持 slots 的特性(无__dict__、省内存),你必须在子类中也显式地定义__slots__。
情况 2:子类也定义了自己的__slots__
如果你在子类中也定义了__slots__,那么它会在父类的基础上添加新的 slots。子类的实例将不再有__dict__。
通常的做法是,子类的__slots__只列出新增的属性即可。
classParent:__slots__=('x','y')classChild(Parent):# 只声明新增的属性 z__slots__=('z',)c=Child()c.x=10# 继承自父类c.y=20# 继承自父类c.z=30# 子类新增的# 现在不能加新属性了c.w=40# AttributeError: 'Child' object has no attribute 'w'print(hasattr(c,'__dict__'))# 输出: False4.2 多重继承的限制
这是一个非常严格的限制。根据 Python 数据模型文档:
Multiple inheritance with multiple slotted parent classes can be used, but only one parent is allowed to have attributes created by slots (the other bases must have empty slot layouts) - violations raise TypeError.
翻译过来就是:
你可以继承多个带有__slots__的父类,但其中只能有一个父类是非空的 slots,其他的父类必须是空的__slots__ = ()。否则会直接报错。
这是因为 Python 的内存布局无法同时处理两个都有实例字段的父类。
错误示例:
classA:__slots__=('a',)classB:__slots__=('b',)classC(A,B):# TypeError!pass运行会报错:TypeError: multiple bases have instance lay-out conflict
正确示例:
classA:__slots__=('a',)classB:__slots__=()# 空的 slotsclassC(A,B):# OK__slots__=('c',)4.3 不要重复定义 Slot
如果子类重新定义了一个父类已经有的 slot,虽然 Python 不会报错,但这会导致父类的那个 slot 变得无法访问,而且会浪费内存。
classParent:__slots__=('x',)classChild(Parent):__slots__=('x',)# 重复定义了!这是一个坏味道,应该避免。
5. 常见问题与高级用法
5.1 如何设置默认值?(避坑重点)
很多新手会尝试用类属性来给 slot 设置默认值,这是一个非常常见的错误!
错误示范与具体后果
# 错误的做法!classPerson:__slots__=('name','age')# 试图用类属性设置默认值name="Unknown"age=0我们环境下的实际运行后果:
ValueError: 'name' in __slots__ conflicts with class variable为什么会这样?
在现代 Python 版本中,解释器已经加入了严格的检查!当你在__slots__中声明了name,同时又在类上定义了同名的类属性时,Python 会直接在类定义阶段就报错,阻止你犯这个错误。
这是因为__slots__是通过描述符(descriptor)实现的,类属性会覆盖描述符,导致 slot 机制失效。现在的 Python 直接拦截了这种错误的写法。
正确的做法
在__init__方法中设置默认值。
classPerson:__slots__=('name','age')def__init__(self,name="Unknown",age=0):self.name=name self.age=age# 现在一切正常p=Person()print(p.name)# Unknownp.name="Bob"print(p.name)# Bob5.2 我还想要动态属性怎么办?
如果你既想享受大部分 slots 带来的内存优化,又想保留一点点动态性,你可以手动把'__dict__'加入到__slots__中!
classMyClass:__slots__=('name','age','__dict__')obj=MyClass()obj.name="Alice"# slotobj.foo="bar"# 动态属性,存在 __dict__ 里这样,声明的name和age依然用 slots 存储,省内存,而额外的属性依然可以存在字典里。
5.3 弱引用支持
默认情况下,定义了__slots__的类不支持弱引用(weakref),因为 Python 去掉了__weakref__属性。
如果你需要支持弱引用,把它加进去就行:
classMyClass:__slots__=('name','__weakref__')5.4 与 Dataclasses 结合
在 Python 3.7+ 的 dataclasses 中,你可以很方便地开启 slots:
fromdataclassesimportdataclass@dataclass(slots=True)# 一行搞定!classPoint:x:inty:int这会自动为你生成带有__slots__的数据类,非常方便。
6. 什么时候不该用__slots__?
- 你需要动态添加属性:如果你的类本身就是高度动态的,那就没必要用它。
- 你需要使用
cached_property:像functools.cached_property这样的装饰器依赖于__dict__来存储缓存结果。 - 实例数量很少:如果你的类只会被实例化几次,那节省的那点内存完全没必要,反而增加了代码的复杂度。
- 需要配合某些特殊的库:有些 ORM 或者序列化库可能依赖于
__dict__。
7. 总结
__slots__是 Python 中一个被低估但极其强大的优化工具。
- 核心作用:通过替换
__dict__,大幅减少内存占用,加速属性访问。 - 基本用法:在类中定义
__slots__ = ('attr1', 'attr2')。 - 继承要点:
- 父类的 slots 会被继承。
- 子类必须也定义
__slots__才能保持无__dict__的特性。 - 多重继承时,只能有一个非空的 slotted 父类。
- 灵活性:你可以通过添加
'__dict__'或'__weakref__'来按需恢复部分功能。 - 避坑提醒:不要用类属性给 slot 设置默认值,现代 Python 会直接报错阻止你!
当你需要处理海量数据对象时,不妨试试给你的类加上__slots__,它往往能给你带来意想不到的性能提升。