Python 3.12 Special Attribute - __slots__
__slots__ 是 Python 中一个用于 优化内存和限制属性 的特殊类属性。通过定义 __slots__,你可以告诉 Python 解释器:该类的实例只允许拥有固定的几个属性(名称在 __slots__ 中列出),并且实例不再拥有默认的 __dict__ 字典和 __weakref__ 弱引用(除非显式加入)。这能显著减少每个实例的内存占用,并提升属性访问速度,尤其适合需要创建大量实例的场景(如游戏对象、数据记录等)。同时,__slots__ 也提供了一种防止动态添加属性的机制。本文将详细解析 __slots__ 的定义、用法、底层原理、与 __dict__ 的关系,并通过多个示例演示其行为。
1. __slots__ 的基本概念
- 定义:
__slots__是一个类属性,其值通常是一个字符串元组(或列表、可迭代对象),列出了该类的实例允许拥有的属性名称。 - 作用:
- 节省内存:实例不再有
__dict__字典,属性存储在类似 C 结构体的固定数组中,避免了字典的开销。 - 防止动态添加属性:实例只能拥有
__slots__中列出的属性,不能动态添加新属性(除非__slots__中包含'__dict__')。 - 提升属性访问速度:由于属性存储位置固定,访问速度略快于字典查找。
- 节省内存:实例不再有
- 默认值:如果类没有定义
__slots__,则实例默认拥有__dict__和__weakref__。
示例:
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y) # 1 2
# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
2. __slots__ 的语法与使用
2.1 基本用法
class Person:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
2.2 包含 '__dict__' 以允许动态属性
class Flexible:
__slots__ = ('fixed', '__dict__')
def __init__(self, fixed):
self.fixed = fixed
f = Flexible(10)
f.dynamic = 20 # 允许,因为有 __dict__
print(f.dynamic) # 20
2.3 包含 '__weakref__' 以支持弱引用
import weakref
class WithWeakref:
__slots__ = ('x', '__weakref__')
def __init__(self, x):
self.x = x
obj = WithWeakref(10)
ref = weakref.ref(obj)
print(ref()) # <__main__.WithWeakref object at ...>
2.4 空 __slots__ 表示实例没有任何属性
class Empty:
__slots__ = ()
e = Empty()
# e.x = 1 # AttributeError
3. 内存对比与性能分析
3.1 内存占用对比(使用 pympler 或 sys.getsizeof)
import sys
class PointDict:
def __init__(self, x, y):
self.x = x
self.y = y
class PointSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
pd = PointDict(1, 2)
ps = PointSlots(1, 2)
print(sys.getsizeof(pd)) # 56 (实例本身)
print(sys.getsizeof(pd.__dict__)) # 112 (属性字典)
print(sys.getsizeof(ps)) # 48 (没有 __dict__)
对于大量实例(如百万个),使用 __slots__ 可以节省数百 MB 内存。
3.2 属性访问速度对比(粗略测试)
由于属性存储位置固定,__slots__ 的访问速度通常比字典查找稍快(大约 10-20%)。但在实际应用中,差异可能不明显。
4. 逐行示例与解析
示例 1:基本内存节省
# 1. 定义无 __slots__ 的类
class WithoutSlots:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
# 2. 定义有 __slots__ 的类
class WithSlots:
__slots__ = ('a', 'b', 'c')
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
# 3. 创建实例
wos = WithoutSlots(1, 2, 3)
ws = WithSlots(1, 2, 3)
# 4. 查看内存占用(近似)
import sys
print(sys.getsizeof(wos)) # 56
print(sys.getsizeof(wos.__dict__)) # 112
print(sys.getsizeof(ws)) # 48
逐行解析:
| 行 | 代码 | 解释 |
|---|---|---|
| 1-5 | 定义 WithoutSlots | 普通类,实例拥有 __dict__。 |
| 7-12 | 定义 WithSlots | 定义 __slots__ = ('a','b','c'),实例没有 __dict__。 |
| 14-15 | 创建实例 | ws 没有 __dict__ 属性。 |
| 18-20 | 打印内存 | ws 实例本身更小,且没有额外的字典开销。 |
为什么这样写?
- 对于需要创建大量实例的场景(如粒子系统、坐标点),使用
__slots__可以显著减少内存使用。
示例 2:阻止动态添加属性
class Restricted:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
r = Restricted(10, 20)
r.z = 30 # AttributeError: 'Restricted' object has no attribute 'z'
逐行解析:
- 因为
__slots__只允许'x'和'y',所以给z赋值会抛出AttributeError。
为什么这样写?
- 用于定义不可变的数据结构或防止属性名拼写错误导致的意外动态属性。
示例 3:继承与 __slots__
子类不会自动继承父类的 __slots__。如果子类也定义了 __slots__,则子类的实例会拥有父类和子类的所有槽位,但仍然没有 __dict__(除非显式加入)。
class Base:
__slots__ = ('x',)
class Derived(Base):
__slots__ = ('y',) # 子类有自己的槽位
d = Derived()
d.x = 1
d.y = 2
# d.z = 3 # AttributeError
如果子类不定义 __slots__,则它会自动拥有 __dict__,从而失去内存优势。
class Derived2(Base):
pass
d2 = Derived2()
d2.z = 100 # 允许,因为 Derived2 有 __dict__
为什么这样写?
- 在继承结构中,如果想继续使用
__slots__,子类必须显式定义__slots__(可以包含父类的槽位,但不是必须的,Python 会自动合并)。
示例 4:包含 __dict__ 以保留动态性
class SemiRestricted:
__slots__ = ('fixed', '__dict__')
def __init__(self, fixed):
self.fixed = fixed
s = SemiRestricted(10)
s.dynamic = 20 # 允许
print(s.dynamic) # 20
为什么这样写?
- 既想保留大部分属性的固定存储以节省内存,又希望少数场景下可以动态添加属性。但此时
__dict__仍然存在,内存节省效果会减弱。
5. 底层实现机制(CPython)
在 CPython 中,每个实例的内存布局由它的类决定。如果类定义了 __slots__,则实例对象在创建时会根据槽位的数量分配固定大小的内存(类似于 C 结构体),每个槽位对应一个 PyObject* 指针。属性值直接存储在这些指针中,而不是通过字典。
具体流程:
- 类定义时,Python 解析
__slots__元组,为每个槽位在类的内部创建一个描述符(member_descriptor),并记录偏移量。 - 实例化时,
type.__call__调用__new__,然后__new__根据__slots__的数量分配实例内存(大小 = 基础对象头 + 槽位数量 * 指针大小)。 - 属性访问
obj.attr被转换为对固定偏移量的读取或写入,而不是字典查找。
内存布局对比:
- 普通实例:
PyObject_HEAD+__dict__指针 +__weakref__指针。 __slots__实例:PyObject_HEAD+ 每个槽位的指针 + 可选的__dict__和__weakref__(如果显式包含)。
性能:由于省去了字典的哈希计算和动态扩容,__slots__ 的属性访问通常更快。
限制:
- 不能为
__slots__中的属性使用@property或描述符?实际上可以,但描述符会覆盖槽位的默认行为。 - 不能有默认值(除非在
__init__中赋值)。
6. 注意事项与陷阱
- 继承陷阱:子类如果不定义
__slots__,会自动获得__dict__,从而失去内存优势。如果需要保留__slots__,子类必须也定义__slots__(可以包括父类的槽位名称,也可以不包含,Python 会自动合并)。 - 多重继承:当多个父类都有
__slots__时,子类的__slots__必须是所有父类槽位的并集,否则会报错(TypeError: multiple bases have instance lay-out conflict)。 - 不能使用默认值:
__slots__只是声明属性名,不能指定默认值。默认值需要在__init__中设置。 - 与弱引用:默认情况下,
__slots__实例不支持弱引用。如果需要,必须显式加入'__weakref__'。 - 序列化:使用
__slots__的类在 pickle 序列化时可能有问题,需要实现__getstate__和__setstate__来处理槽位属性。 - 调试:
__slots__实例没有__dict__,因此vars(obj)会失败(TypeError)。调试时可以用dir(obj)查看属性。
7. 使用场景总结
| 场景 | 是否推荐使用 __slots__ |
|---|---|
| 需要创建大量实例(百万级以上) | ✅ 强烈推荐 |
| 内存受限的环境(如嵌入式设备) | ✅ 推荐 |
| 需要防止动态添加属性 | ✅ 推荐 |
| 需要支持弱引用 | 需显式包含 '__weakref__' |
| 需要动态添加属性 | ❌ 不推荐(除非包含 '__dict__') |
| 类会被频繁继承且子类也需要优化 | ⚠️ 需谨慎设计,子类也要定义 __slots__ |
使用 pickle 序列化 | ⚠️ 需要额外处理 |
8. 总结
| 特性 | 说明 |
|---|---|
| 角色 | 限制实例属性,优化内存 |
| 类型 | 类属性,值通常为元组 |
| 存储方式 | 属性存储在固定偏移量,无 __dict__ |
| 内存节省 | 每个实例节省一个字典的开销(约 72 字节 + 条目开销) |
| 动态属性 | 默认禁止,可通过包含 '__dict__' 允许 |
| 弱引用 | 默认不支持,需包含 '__weakref__' |
| 继承 | 子类需显式定义 __slots__ 才能保留优化 |
| 典型用途 | 大量实例的数据类、游戏对象、数值点 |
| 最佳实践 | 在性能敏感且实例数量巨大的场景使用;避免滥用;注意继承兼容性 |
掌握 __slots__ 是 Python 内存优化和高级类设计的重要技能。通过合理使用,你可以大幅降低程序的内存占用,同时获得一定的性能提升。但请记住,“过早优化是万恶之源”,在确定内存瓶颈后再使用 __slots__。希望本文能帮助你全面理解这一特殊属性。
如果在学习过程中遇到问题,欢迎在评论区留言讨论!
1782

被折叠的 条评论
为什么被折叠?



