Python 类中的 __slots__ 节省内存
python class Point: def init(self, x, y): self.x = x self.y = y
p = Point(1.0, 2.0) print(p.dict) # {'x': 1.0, 'y': 2.0}
每个 `Point` 实例都会有一个独立的字典 `__dict__`,用于存放 `x` 和 `y`。定义一千个 `Point` 对象,就会分配一千个字典。
我们可以用 `sys.getsizeof()` 来观察对象的大致大小:
```python
import sys
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1.0, 2.0)
print(sys.getsizeof(p)) # 基础对象大小
print(sys.getsizeof(p.__dict__)) # 字典大小
实际内存消耗远不止 getsizeof 显示的数字,因为它不计算字典内部键值对引用的对象本身。但可以看到每个实例都会额外持有一个不小的字典结构。
使用 __slots__ 节省内存
基本用法
在类中定义 __slots__ 为一个包含属性名字符串的可迭代对象(一般用元组)。这告诉 Python:这个类的实例只允许有这些属性,且不再创建 __dict__。
class PointWithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p2 = PointWithSlots(1.0, 2.0)
print(p2.x, p2.y) # 正常访问
# print(p2.__dict__) # AttributeError: 'PointWithSlots' object has no attribute '__dict__'
尝试给实例添加未声明的属性会直接抛出 AttributeError:
p2.z = 3.0 # AttributeError
这种显示声明属性的方式,同时带来了防止属性拼写错误的好处。
内存对比测试
我们可以用更精确的工具对比内存占用:
from pympler import asizeof
class PointNormal:
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
normal_objects = [PointNormal(i, i) for i in range(100000)]
slots_objects = [PointSlots(i, i) for i in range(100000)]
print("普通对象总大小:", asizeof.asizeof(normal_objects))
print("__slots__ 对象总大小:", asizeof.asizeof(slots_objects))
测试结果会显示,使用 __slots__ 后总内存占用可能减少 40% ~ 60%。对于单个实例,节省的比例甚至更高,因为对象的固定开销中字典占了一大块。当实例数量庞大时,节省的内存非常可观。
__slots__ 的高级特性与注意事项
1. __slots__ 与继承
子类若没有定义 __slots__,会重新获得 __dict__,并且可以任意添加属性。
class Shape:
__slots__ = ('name',)
class Circle(Shape):
pass # 没有定义 __slots__
c = Circle()
c.name = "circle"
c.radius = 5 # 可以添加,因为有了 __dict__
如果希望子类也保持无字典的特性,子类必须也定义 __slots__,其中包含父类的插槽和自身新增的插槽。
class Circle(Shape):
__slots__ = ('radius',) # 包含了父类的 'name' 吗?不,只需要写子类新增的
def __init__(self, name, radius):
self.name = name # name 来自父类 __slots__
self.radius = radius
关键规则:子类 __slots__ 的值会自动与父类的 __slots__ 合并。所以子类只需声明自己独有的属性名即可。
如果父类没有 __slots__,子类就算定义了 __slots__ 也无法阻止 __dict__ 的创建。因为父类实例布局依赖于字典,子类必须兼容。
2. 弱引用和 __weakref__ 插槽
默认情况下,定义了 __slots__ 的类不允许对实例进行弱引用(weakref)。如果你需要使用弱引用,必须显式把 __weakref__ 添加到 __slots__ 中:
import weakref
class Node:
__slots__ = ('value', '__weakref__')
def __init__(self, value):
self.value = value
n = Node(10)
ref = weakref.ref(n)
print(ref()) # 打印 Node 对象