Python 3.12 Special Attribute - 11 - __slots__

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

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 内存占用对比(使用 pymplersys.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* 指针。属性值直接存储在这些指针中,而不是通过字典。

具体流程

  1. 类定义时,Python 解析 __slots__ 元组,为每个槽位在类的内部创建一个描述符(member_descriptor),并记录偏移量。
  2. 实例化时,type.__call__ 调用 __new__,然后 __new__ 根据 __slots__ 的数量分配实例内存(大小 = 基础对象头 + 槽位数量 * 指针大小)。
  3. 属性访问 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__。希望本文能帮助你全面理解这一特殊属性。

如果在学习过程中遇到问题,欢迎在评论区留言讨论!

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AsDuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值