为何带__slots__的对象pickle序列化后体积比不带的更大?
问题:使用__slots__后pickle序列化体积反而增大的原因
我正在开发的程序频繁因OOM killer崩溃,想在不进行大规模重构的前提下快速降低内存占用。给最常用的类添加__slots__后,却发现其pickle序列化后的体积反而增加了,这是为什么?
class Class: def __init__(self, a, b, c): self.a = a self.b = b self.c = c class ClassSlots: __slots__ = ["a", "b", "c"] def __init__(self, a, b, c): self.a = a self.b = b self.c = c cases = [ Class(1, 2, 3), ClassSlots(1, 2, 3), [Class(1, 2, 3) for _ in range(1000)], [ClassSlots(1, 2, 3) for _ in range(1000)] ] for case in cases: dump = pickle.dumps(case, protocol=5) print(len(dump))
在Python 3.10中运行上述代码,输出如下:
59 67 22041 25046
原因解析
这是因为普通类的实例在pickle序列化时,默认会打包存储实例的__dict__属性字典,而pickle协议5对字典序列化做了关键优化:对于重复出现的键(比如多个实例的a、b、c),只会存储一次键名,后续实例直接复用这个引用,大幅减少了重复数据的体积。
而使用__slots__的类,实例没有__dict__属性,pickle需要逐个序列化每个槽位的属性,并且每个实例都要重复存储槽位的属性名(即使多个实例的属性名完全一致)。这就导致单个实例的序列化体积更大,当序列化大量实例组成的列表时,重复的属性名会累积出更明显的体积差异。
简单总结:普通类靠字典的键复用优化压缩了体积,而__slots__类因为没有__dict__,吃不到这个优化,反而要重复存储属性名,所以序列化体积反而增大。
折中方案
如果既要保留__slots__的内存节省优势,又想优化pickle体积,可以尝试以下方法:
- 自定义
__getstate__和__setstate__方法,手动控制序列化内容,比如把槽位属性打包成元组或带共享键的字典,让pickle能复用属性名引用。示例代码:
这样处理后,元组的序列化体积会远小于逐个存储属性名的方式,大量实例列表的序列化体积也会接近普通类的水平。class ClassSlots: __slots__ = ["a", "b", "c"] def __init__(self, a, b, c): self.a = a self.b = b self.c = c def __getstate__(self): # 将槽位属性转为元组,减少序列化时的重复数据 return (self.a, self.b, self.c) def __setstate__(self, state): self.a, self.b, self.c = state - 升级到Python 3.11及以上版本,新版本的pickle对
__slots__类的序列化做了针对性优化,能有效减少这类体积膨胀问题。
内容的提问来源于stack exchange,提问作者parched
相关产品推荐
相关产品推荐

