Python 中的 `__dict__` 与 `__slots__` 深度解析
2026/5/7 8:51:32 网站建设 项目流程

一、对象属性存储的本质

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 按如下顺序查找:

  1. type(obj).__mro__中各类的__dict__里是否有数据描述符(定义了__get____set__
  2. obj.__dict__中是否有该属性
  3. 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:float

Python 3.10+ 的dataclass支持slots=True参数,自动处理:

@dataclass(slots=True)classVector3D:x:floaty:floatz:float

7.3 与 Pydantic 配合

frompydanticimportBaseModelclassConfig(BaseModel):model_config={'frozen':True}# Pydantic v2 内部使用 __slots__ 优化host:strport:int

Pydantic 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__

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询