Python __slots__ 入门指南

在 Python 中,我们习惯了对象的动态特性 ------ 可以随时给实例添加新的属性。这非常灵活,但在处理大量数据对象时,这种灵活性会带来不小的内存开销。__slots__ 正是为了解决这个问题而生的强大工具。

本教程将带你全面了解 __slots__ 的功能、基本用法以及在继承场景下的注意事项,所有代码示例均经过实际运行验证,确保结果真实可靠。

1. 什么是 __slots__

__slots__ 是 Python 类中的一个特殊类属性(class attribute)。

默认情况下,Python 类的实例会有一个 __dict__ 属性,这是一个字典,用来存储实例的所有属性。这个字典允许我们在运行时动态地添加新属性,但它本身也会消耗大量的内存。

当你在类中定义了 __slots__ 时,Python 会:

  1. 为声明的属性在内存中预留固定的空间。
  2. 不再为每个实例自动创建 __dict____weakref__
  3. 限制实例只能拥有 __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__ 里

这样,声明的 nameage 依然用 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__,它往往能给你带来意想不到的性能提升。

相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第69题】【JVM篇】第29题:GC Roots 有哪些?
java·开发语言·jvm·面试
小白|1 小时前
tensorflow:昇腾CANN的TensorFlow适配层
人工智能·python·tensorflow
Matlab程序猿小助手1 小时前
【MATLAB源码-第319期】基于matlab的帝王蝶优化算法(MBO)无人机三维路径规划,输出做短路径图和适应度曲线.
开发语言·算法·matlab
码点滴1 小时前
CRI-O选型与容器运行时标准
开发语言·人工智能·架构·kubernetes·cri-o
回眸&啤酒鸭1 小时前
【回眸】嵌入式软件单元测试工具链实战指南
开发语言·单元测试·白盒测试
彦为君1 小时前
JavaSE-10-并发编程(11个案例)
java·开发语言·python·ai·nio
石山代码1 小时前
java前景
java·开发语言
10岁的博客1 小时前
C++ 进制转换:通用 a 进制转 b 进制(2-36进制)题解
开发语言·c++
Cthy_hy2 小时前
树状数组(BIT)进阶:差分优化实现区间修改、区间查询
数据结构·python·算法