一、对象属性存储的本质
Python 是一门动态语言,每个对象的属性默认存储在一个字典中——这就是__dict__。这种设计赋予了 Python 极大的灵活性,但也带来了内存和性能上的代价。__slots__则是 Python 提供的一种优化机制,用固定的描述符替代字典存储。
二、__dict__详解
2.1 工作原理
每个普通 Python 对象都拥有一个__dict__属性,它是一个标准字典,存储实例的所有属性:
classUser:def__init__(self,name,age):self.name=name self.age=age u=User("Alice",30)print(u.__dict__)# {'name': 'Alice', 'age': 30}属性的读写本质上是字典操作:
u.email="alice@example.com"# 等价于 u.__dict__['email'] = ...print(u.__dict__)# {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}2.2 类的__dict__vs 实例的__dict__
classAnimal:species="Unknown"# 类属性,存储在 Animal.__dict__def__init__(self,name):self.name=name# 实例属性,存储在 self.__dict__a=Animal("Cat")print(Animal.__dict__)# mappingproxy({'species': 'Unknown', '__init__': <function ...>, ...})print(a.__dict__)# {'name': 'Cat'}类的__dict__是一个mappingproxy(只读视图),存储类方法、类属性和描述符;实例的__dict__是普通dict。
2.3 属性查找链(MRO)
当访问obj.attr时,Python 按如下顺序查找:
type(obj).__mro__中各类的__dict__里是否有数据描述符(定义了__get__和__set__)obj.__dict__中是否有该属性type(obj).__mro__中各类的__dict__里是否有非数据描述符或普通属性
2.4 内存开销
每个实例的__dict__是一个完整的dict对象。在 CPython 3.3+ 中引入了Key-Sharing Dictionary(PEP 412)优化:同一类的实例如果在__init__中以相同顺序创建相同属性,则共享 key 数组,仅存储 values 数组。
即便如此,每个dict对象本身仍有 ~56 字节的固定开销(CPython 3.11 64位),加上哈希表预分配的容量。当你有百万级实例时,这非常可观。
三、__slots__详解
3.1 基本用法
classPoint:__slots__=('x','y')def__init__(self,x,y):self.x=x self.y=y p=Point(1,2)print(p.x)# 1p.z=3# AttributeError: 'Point' object has no attribute 'z'print(hasattr(p,'__dict__'))# False定义__slots__后:
- 实例不再拥有
__dict__ - 属性以固定偏移量存储在对象结构体中
- 不能动态添加未声明的属性
3.2 底层实现
当类定义了__slots__,Python 为每个 slot 名称在类上创建一个member descriptor(类似property):
print(Point.x)# <member 'x' of 'Point' objects>print(type(Point.x))# <class 'member_descriptor'>这些描述符直接通过 C 级别的固定偏移量(tp_members)读写对象内存,无需哈希计算,速度比字典查找更快。
3.3 内存对比
importsysclassDictPoint:def__init__(self,x,y):self.x=x self.y=yclassSlotPoint:__slots__=('x','y')def__init__(self,x,y):self.x=x self.y=y d=DictPoint(1,2)s=SlotPoint(1,2)print(sys.getsizeof(d)+sys.getsizeof(d.__dict__))# ~152 bytes (CPython 3.11)print(sys.getsizeof(s))# ~48 bytes节省约 60-70% 的内存。对于大规模数据对象(如 ORM 行、科学计算粒子),效果显著。
3.4 性能优势
属性访问基准测试(CPython 3.11, 100M 次读取):
| 操作 | __dict__ | __slots__ | 提升 |
|---|---|---|---|
| 读取属性 | ~45ns | ~35ns | ~22% |
| 写入属性 | ~55ns | ~40ns | ~27% |
提升幅度取决于 CPython 版本和内联缓存优化程度。在 3.12+ 的 specializing interpreter 下差距缩小,但 slots 仍有优势。
四、__slots__的继承规则
4.1 父类无 slots,子类有 slots
classBase:pass# 有 __dict__classChild(Base):__slots__=('x',)c=Child()c.x=1# 使用 slotc.y=2# 仍然可以,因为继承了 __dict__print(c.__dict__)# {'y': 2}陷阱:只要继承链中任何一个类没有__slots__,实例就会有__dict__,内存优化效果大打折扣。
4.2 多层 slots 继承
classA:__slots__=('a',)classB(A):__slots__=('b',)# 不要重复声明 'a'b=B()b.a=1# 来自 A 的 slotb.b=2# 来自 B 的 slot规则:子类的__slots__只需声明新增的属性。重复声明会导致多个同名 descriptor,虽然不报错但浪费内存。
4.3 多重继承限制
classX:__slots__=('x',)classY:__slots__=('y',)classZ(X,Y):# OK: 多个非空 slots 父类,且无冲突的内存布局__slots__=('z',)但如果多个父类定义了非空__slots__且存在非平凡的 C 级布局冲突,会抛出TypeError。实践中的安全规则:多重继承时最多一个父类有非空__slots__。
五、__slots__与__dict__共存
可以在__slots__中显式包含'__dict__':
classHybrid:__slots__=('x','__dict__')def__init__(self,x):self.x=x# 走 sloth=Hybrid(1)h.y=2# 走 __dict__print(h.__dict__)# {'y': 2}这样x享受 slot 的性能优势,同时保留动态属性能力。适合"核心属性固定,但允许扩展"的场景。
六、__slots__与__weakref__
默认情况下,slots 类实例不支持弱引用:
importweakrefclassNoWeak:__slots__=('x',)weakref.ref(NoWeak())# TypeError: cannot create weak referenceclassWithWeak:__slots__=('x','__weakref__')weakref.ref(WithWeak())# OK如果需要弱引用支持,必须在__slots__中包含'__weakref__'。
七、实战场景与最佳实践
7.1 何时使用__slots__
| 场景 | 推荐 |
|---|---|
| 大量同质实例(>10k 对象) | ✅ 强烈推荐 |
| 数据传输对象(DTO/VO) | ✅ 推荐 |
| ORM 模型行 | ⚠️ 视框架支持 |
| 需要动态添加属性 | ❌ 不适合 |
需要__dict__做序列化 | ⚠️ 需额外处理 |
| Mixin / 抽象基类 | ❌ 通常不适合 |
7.2 与 dataclasses 配合
fromdataclassesimportdataclass@dataclassclassVector3D:__slots__=('x','y','z')x:floaty:floatz:floatPython 3.10+ 的dataclass支持slots=True参数,自动处理:
@dataclass(slots=True)classVector3D:x:floaty:floatz:float7.3 与 Pydantic 配合
frompydanticimportBaseModelclassConfig(BaseModel):model_config={'frozen':True}# Pydantic v2 内部使用 __slots__ 优化host:strport:intPydantic v2 的BaseModel已经在内部使用 slots 优化核心字段存储。
7.4 序列化注意事项
pickle默认依赖__dict__。对 slots 类需要实现__getstate__/__setstate__:
classSlotObj:__slots__=('a','b')def__getstate__(self):return{slot:getattr(self,slot)forslotinself.__slots__ifhasattr(self,slot)}def__setstate__(self,state):forslot,valueinstate.items():setattr(self,slot,value)八、CPython 内部视角
8.1 对象内存布局
┌────────────────────────────────────┐ │ PyObject_HEAD (ob_refcnt, ob_type)│ 16 bytes ├────────────────────────────────────┤ │ __dict__ pointer │ 8 bytes (无 slots 时) │ __weakref__ pointer │ 8 bytes (无 slots 时) ├────────────────────────────────────┤ │ slot_0 (PyObject*) │ 8 bytes (有 slots 时) │ slot_1 (PyObject*) │ 8 bytes │ ... │ └────────────────────────────────────┘使用__slots__时,属性直接作为PyObject*指针紧凑排列在对象体内,消除了字典的哈希表、key 数组和间接寻址开销。
8.2 Key-Sharing Dict (PEP 412)
CPython 3.3+ 对同一类型的实例__dict__做了优化:
Instance A.__dict__ ──┐ Instance B.__dict__ ──┼──► Shared Keys Array: ['name', 'age', 'email'] Instance C.__dict__ ──┘ Values A: [ptr, ptr, ptr] Values B: [ptr, ptr, ptr] Values C: [ptr, ptr, ptr]这减少了内存使用,但仍无法与__slots__的紧凑布局相比,因为每个实例仍需一个dict对象头和 values 数组。
九、常见陷阱
9.1 默认可变值
classWrong:__slots__=('items',)items=[]# 类属性会与 slot descriptor 冲突!# 正确做法:在 __init__ 中赋值classRight:__slots__=('items',)def__init__(self):self.items=[]9.2 忘记在子类重声明__slots__
classParent:__slots__=('x',)classChild(Parent):pass# 没有 __slots__,Child 实例会获得 __dict__,失去优化9.3 与元类/装饰器冲突
某些框架的元类会操作__dict__,使用__slots__时需验证兼容性。例如早期 SQLAlchemy 的 declarative base 与__slots__不兼容。
十、总结对比
| 特性 | __dict__ | __slots__ |
|---|---|---|
| 内存占用 | 较高(~104-152 bytes/实例) | 较低(~48-64 bytes/实例) |
| 属性访问速度 | 哈希查找 | 固定偏移量 |
| 动态添加属性 | ✅ | ❌(除非包含__dict__) |
| 弱引用支持 | ✅ 默认 | 需显式声明__weakref__ |
| 继承复杂度 | 低 | 高(需每层声明) |
| 序列化兼容 | ✅ 天然支持 | 需额外实现 |
| 适用场景 | 通用、灵活 | 大量实例、性能敏感 |
核心原则:__dict__是 Python 动态性的基石,适合绝大多数场景;__slots__是有针对性的优化手段,在明确需要节省内存或提升属性访问性能时启用。不要过早优化——先 profile,再决定是否引入__slots__。