Python __slots__ 继承:父类限制了字段,为什么子类还能随手增加属性
基础记录类用 __slots__ 固定了 name 字段,给对象多写一个 note 会报错。后来只是加了一个不含任何代码的子类,同样的 note 赋值却成功了。父类的槽位没有失效:子类仍然继承那些字段,同时因为没有声明自己的 __slots__,又获得了存放额外属性的实例字典。
判断对象能否增加属性,需要看完整继承关系,不能只搜索某个父类里有没有槽位声明。下面把三个子类放在一起,分别观察自动扩展、保持限制和明确增加字段。代码仅操作内存,保存为 demo.py 后执行 python demo.py,所有断言都针对具体属性行为,不依赖机器上的对象字节数。
AI生成概念插图:固定格子的托盘与外接收纳盒并列,表示继承槽位和新增动态存储可以同时存在;不是软件界面或运行截图。
class Base:
__slots__ = ("name",)
def __init__(self, name):
self.name = name
class Loose(Base):
pass
class Tight(Base):
__slots__ = ()
class Extended(Base):
__slots__ = ("count",)
loose = Loose("demo")
loose.note = "extra"
assert loose.__dict__ == {"note": "extra"}
assert loose.name == "demo"
print("Loose dict:", loose.__dict__)
tight = Tight("demo")
assert not hasattr(tight, "__dict__")
try:
tight.note = "extra"
except AttributeError:
print("Tight extra field: rejected")
else:
raise AssertionError("unexpected dynamic field")
extended = Extended("demo")
extended.count = 3
assert (extended.name, extended.count) == ("demo", 3)
assert not hasattr(extended, "__dict__")
print("Extended:", extended.name, extended.count)空子类也会改变存储方式
Loose 没有定义槽位,所以 note 被写进实例的 __dict__。输出字典只有 note,并没有继承来的 name;name 仍存放在父类声明的槽位里。这说明“实例有字典”不等于“实例的全部属性都在字典中”。如果用 vars 导出对象,单看字典会漏掉仍由槽位管理的字段。
Tight 写了空元组,意思是这个子类不新增槽位,也不因为省略声明而自动引入字典。它仍然拥有 Base 的 name,给 note 赋值却抛出 AttributeError。不要把空槽位理解成不继承任何属性,它限制的是这一层新添的存储安排。
新增字段只声明新增的名字
Extended 在自己的槽位中列出 count,因此可同时读写 name 和 count,但仍没有实例字典。子类只应声明自己新增的槽位,不要为了看起来完整,把 name 再重复抄一遍。官方文档指出,与父类重名的槽位会遮蔽父类对应的槽位,让程序语义出现问题。
也别给槽位同名的类属性直接安排默认值。槽位通过类上的描述符提供访问,随意占用同名位置会与它冲突。需要默认 name 或 count 时,通常应在初始化方法里给实例赋值,并覆盖未赋值就读取的测试,避免把声明过字段误当成字段已经拥有有效内容。
如果继承链中较早的普通父类已经提供 __dict__,后来再加空槽位也不能把那份字典收回。相反,确实需要动态扩展时,可以有意把 __dict__ 纳入合适的槽位设计。两种选择都应写清楚,让后续维护者知道额外属性是允许的扩展点,还是无意间重新打开的入口。
槽位常用来减少大量实例的存储开销,但这份示例只证明属性是否可写,没有测量节省多少内存。不同解释器、继承层次和引用内容都会影响总成本。若目标是优化,应拿真实数量和真实字段测量;若目标是发现拼写错误,则应直接测试非法属性写入是否确实被拒绝。
最后还要分清槽位限制与不可变对象。Tight 的 name 仍可重新赋值,槽位也不会自动验证字符串长度或业务状态。需要不可变性、类型约束或序列化规则时,应分别实现并验收。把多个要求都寄托在一个 __slots__ 声明上,会让接口表现与维护者的预期逐渐分离。
资料核对日期:2026年10月2日(北京时间)。示例在本地CPython 3.12.14实际运行并通过断言,结果对应文中固定输入。


