在 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 精确测量):
python
import tracemalloc
# 1. 普通类
class NormalPoint:
def __init__(self, x, y):
self.x = x
self.y = y
# 2. 使用 __slots__ 的类
class SlotPoint:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# 测试创建 10 万个实例的内存占用
def test_memory(cls, count=100000):
tracemalloc.start()
instances = [cls(i, i+1) for i in range(count)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
total = sum(stat.size for stat in top_stats)
tracemalloc.stop()
return total
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),列出你允许实例拥有的所有属性名。
python
class Point:
# 声明允许的属性
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# 正常使用
p = Point(10, 20)
print(p.x) # 输出: 10
print(p.y) # 输出: 20
3.2 动态属性被禁止
尝试添加一个未声明的属性会发生什么?
python
# 尝试添加新属性 z
p.z = 30
运行结果:
AttributeError: 'Point' object has no attribute 'z'
这正是我们想要的!它阻止了意外的属性创建。对比一下普通类的行为:
python
class NormalPoint:
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__:
python
print(hasattr(p, '__dict__')) # 输出: False
4. 继承中的注意事项
__slots__ 的行为在继承中会变得稍微复杂一些,这也是最容易出错的地方。根据 Python 官方文档,我们需要注意以下几点:
4.1 单继承:子类是否定义 __slots__?
父类定义的 __slots__ 会自动继承给子类。但是,子类是否自己定义 __slots__ 会导致完全不同的结果。
情况 1:子类没有定义自己的__slots__
如果父类有 __slots__,但子类没有,那么子类的实例仍然会拥有 __dict__!
这意味着,虽然子类继承了父类的 slots,但你依然可以给子类实例动态添加新属性,因为它有字典。这也意味着,你失去了大部分的内存优化效果。
python
class Parent:
__slots__ = ('x', 'y')
class Child(Parent):
# 注意:这里没有定义 __slots__
pass
c = Child()
c.x = 10
c.y = 20
c.z = 30 # 这居然是允许的!因为 Child 有 __dict__
print(hasattr(c, '__dict__')) # 输出: True
print(c.__dict__) # 输出: {'z': 30}
如果你想让子类也保持 slots 的特性(无__dict__、省内存),你必须在子类中也显式地定义__slots__。
情况 2:子类也定义了自己的__slots__
如果你在子类中也定义了 __slots__,那么它会在父类的基础上添加新的 slots。子类的实例将不再有__dict__。
通常的做法是,子类的 __slots__ 只列出新增的属性即可。
python
class Parent:
__slots__ = ('x', 'y')
class Child(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__')) # 输出: False
4.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 的内存布局无法同时处理两个都有实例字段的父类。
错误示例:
python
class A:
__slots__ = ('a',)
class B:
__slots__ = ('b',)
class C(A, B): # TypeError!
pass
运行会报错:TypeError: multiple bases have instance lay-out conflict
正确示例:
python
class A:
__slots__ = ('a',)
class B:
__slots__ = () # 空的 slots
class C(A, B): # OK
__slots__ = ('c',)
4.3 不要重复定义 Slot
如果子类重新定义了一个父类已经有的 slot,虽然 Python 不会报错,但这会导致父类的那个 slot 变得无法访问,而且会浪费内存。
python
class Parent:
__slots__ = ('x',)
class Child(Parent):
__slots__ = ('x',) # 重复定义了!
这是一个坏味道,应该避免。
5. 常见问题与高级用法
5.1 如何设置默认值?(避坑重点)
很多新手会尝试用类属性来给 slot 设置默认值,这是一个非常常见的错误!
错误示范与具体后果
python
# 错误的做法!
class Person:
__slots__ = ('name', 'age')
# 试图用类属性设置默认值
name = "Unknown"
age = 0
我们环境下的实际运行后果:
ValueError: 'name' in __slots__ conflicts with class variable
为什么会这样?
在现代 Python 版本中,解释器已经加入了严格的检查!当你在__slots__ 中声明了name,同时又在类上定义了同名的类属性时,Python 会直接在类定义阶段就报错,阻止你犯这个错误。
这是因为 __slots__ 是通过描述符(descriptor)实现的,类属性会覆盖描述符,导致 slot 机制失效。现在的 Python 直接拦截了这种错误的写法。
正确的做法
在 __init__ 方法中设置默认值。
python
class Person:
__slots__ = ('name', 'age')
def __init__(self, name="Unknown", age=0):
self.name = name
self.age = age
# 现在一切正常
p = Person()
print(p.name) # Unknown
p.name = "Bob"
print(p.name) # Bob
5.2 我还想要动态属性怎么办?
如果你既想享受大部分 slots 带来的内存优化,又想保留一点点动态性,你可以手动把 '__dict__' 加入到__slots__ 中!
python
class MyClass:
__slots__ = ('name', 'age', '__dict__')
obj = MyClass()
obj.name = "Alice" # slot
obj.foo = "bar" # 动态属性,存在 __dict__ 里
这样,声明的 name 和age 依然用 slots 存储,省内存,而额外的属性依然可以存在字典里。
5.3 弱引用支持
默认情况下,定义了 __slots__ 的类不支持弱引用(weakref),因为 Python 去掉了__weakref__ 属性。
如果你需要支持弱引用,把它加进去就行:
python
class MyClass:
__slots__ = ('name', '__weakref__')
5.4 与 Dataclasses 结合
在 Python 3.7+ 的 dataclasses 中,你可以很方便地开启 slots:
python
from dataclasses import dataclass
@dataclass(slots=True) # 一行搞定!
class Point:
x: int
y: 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__,它往往能给你带来意想不到的性能提升。