Python __slots__ 对内存占用的优化原理
在创建海量对象(例如百万级实例)时,你可能会发现程序占用的内存超乎预期。这往往是因为Python默认的每个实例都持有一份独立的属性字典。理解并正确使用__slots__,能帮你节省大量内存,让程序运行得更轻快。
一、 原理:理解默认的属性字典
在标准的Python类中,每个实例的属性并非直接存储在对象本身,而是存储在一个被称为属性字典(__dict__)的独立结构中。
观察 一个常规类的内存布局:
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
# 创建两个实例
p1 = RegularPoint(1, 2)
p2 = RegularPoint(3, 4)
# 每个实例都有自己独立的属性字典
print(p1.__dict__) # 输出: {'x': 1, 'y': 2}
print(p2.__dict__) # 输出: {'x': 3, 'y': 4}
问题 就在于,每个RegularPoint实例(p1, p2)除了存储x和y的值,都额外维护了一个完整的字典对象来存储这些属性。字典本身是一种为了灵活而牺牲内存效率的数据结构。当创建成千上万个这样的实例时,这些字典带来的内存开销会非常巨大。
二、 核心:使用 __slots__ 固化属性
__slots__的出现,就是为了解决上述内存浪费问题。它在类定义中通过一个元组(或列表)预先声明该类实例所能拥有的所有属性。
声明 一个使用__slots__的类:
class SlottedPoint:
__slots__ = (‘x‘, ‘y‘) # 关键步骤:声明允许的属性名
def __init__(self, x, y):
self.x = x
self.y = y
# 创建实例
sp1 = SlottedPoint(1, 2)
sp2 = SlottedPoint(3, 4)
关键变化 发生在内存布局上:
- 移除
__dict__:实例不再拥有__dict__属性。尝试访问sp1.__dict__会引发AttributeError。 - 属性直接存储:属性
x和y的值,被直接存储在实例对象本身的内存结构中,通常通过描述符机制实现,避免了额外字典的开销。 - 属性列表锁定:你只能给实例赋值
__slots__元组中声明的属性。尝试赋值一个未声明的属性(如sp1.z = 3)会立即报错。这虽然牺牲了一些灵活性,但换来了内存和访问速度的优化。
三、 实验:量化内存节省效果
理论不如实践。我们可以通过sys.getsizeof和一个简单循环来直观对比两种模式的内存占用。
执行 以下代码来测量内存:
import sys
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint:
__slots__ = (‘x‘, ‘y‘)
def __init__(self, x, y):
self.x = x
self.y = y
# 测量单个实例的基础内存占用(不包括其属性字典的开销)
regular_size = sys.getsizeof(RegularPoint(0, 0))
slotted_size = sys.getsizeof(SlottedPoint(0, 0))
print(f“单个实例基础对象大小:“)
print(f“ 常规类 (RegularPoint): {regular_size} 字节“)
print(f“ 带slots类 (SlottedPoint): {slotted_size} 字节“)
# 测量批量创建实例后的总内存估算(通过字典推导式的大小来间接感受)
import tracemalloc
def measure_memory(Class, n):
tracemalloc.start()
instances = [Class(i, i) for i in range(n)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current
N = 100000
mem_regular = measure_memory(RegularPoint, N)
mem_slotted = measure_memory(SlottedPoint, N)
print(f“\n创建 {N} 个实例的近似内存占用:“)
print(f“ 常规类: {mem_regular / 1024:.2f} KB“)
print(f“ 带slots类: {mem_slotted / 1024:.2f} KB“)
预期结果 会清晰地显示:SlottedPoint实例的基础对象大小小于RegularPoint,并且在创建大量实例时,总体内存占用有显著下降。下降幅度取决于属性数量和实例总数,有时可达到40%-50%。
四、 进阶:理解混合模式与继承
在使用__slots__时,有几个关键规则需要牢记,否则优化可能失效。
规则1:父类与子类
- 如果父类没有定义
__slots__,那么即使子类定义了__slots__,子类实例依然会拥有__dict__,优化效果大打折扣。 - 最佳实践是,从不使用任何属性字典的基类(可以是一个只有
__slots__的空类)开始继承。
class Base:
__slots__ = () # 空基类,作为优化的起点
class Derived(Base):
__slots__ = (‘value‘,)
d = Derived()
d.value = 10
# d.__dict__ 会报错,优化生效
规则2:__slots__ 与 __dict__ 的混合
你可以在__slots__列表中包含‘__dict__‘。这会让实例同时拥有固定的槽位和一个属性字典。这是一种折中方案,既为常用属性优化内存,又保留了动态添加其他属性的灵活性。
规则3:弱引用支持
如果希望实例能被弱引用(weakref),你需要在__slots__元组中显式加入‘__weakref__‘。
import weakref
class SlottedWithWeakref:
__slots__ = (‘x‘, ‘__weakref__‘)
obj = SlottedWithWeakref()
obj.x = 1
ref = weakref.ref(obj) # 现在可以正常创建弱引用
五、 应用:何时使用 __slots__
__slots__并非万能药,应明智使用。
推荐使用场景:
- 需要创建大量实例:这是最主要的场景。例如,游戏中的子弹、粒子系统,数据处理中的数据点、记录对象,Web服务中的请求上下文对象等。
- 属性固定且已知:类的属性集合在设计时就完全确定,未来不需要动态添加。
- 对内存敏感的环境:嵌入式设备、移动应用或需要处理超大规模数据的后台服务。
谨慎或避免使用场景:
- 需要动态添加属性:比如ORM对象、某些需要灵活性的元编程场景。
- 属性会变化:类的设计可能在未来需要添加新属性。
- 频繁使用多重继承:
__slots__在多重继承中的行为较为复杂,容易出错。
最后一步:在你的项目中,定位创建对象最密集、数量最庞大的类。分析其属性是否固定。如果满足条件,应用 __slots__ 并测量优化前后的内存占用变化。

暂无评论,快来抢沙发吧!