__slots__ 不能代替不可变性,因其仅限制属性名而非禁止赋值;真正不可变需冻结字段且递归处理嵌套可变对象,推荐用 @dataclass(frozen=True) 配合不可变类型。

为什么 __slots__ 不能代替不可变性
很多人误以为给类加上 __slots__ 就能防止属性被修改,其实它只限制实例能拥有的属性名,并不阻止对已有属性的赋值。比如定义了 __slots__ = ['x', 'y'] 后,obj.x = 10 依然合法——这离“不可变”差得远。
真正需要的是:初始化后任何字段都不能再被修改,包括嵌套可变对象(如列表、字典)的内容也不能意外变更。
- 仅靠
__slots__+ 手动设__setattr__抛错,无法防御内部可变对象的原地修改(例如obj.data.append(1)) - 若字段是
list或dict,必须用不可变替代品(如tuple、frozenset、types.MappingProxyType) - 自定义
__setattr__时需绕过父类逻辑,否则object.__setattr__(self, name, value)会失效
用 __slots__ + __setattr__ 实现基础不可变控制
最轻量的方式是禁用所有赋值,但允许在 __init__ 中一次性设置。关键在于区分“初始化阶段”和“后续赋值”。
推荐做法是在 __init__ 结束前设一个标记位,之后所有 __setattr__ 调用都检查该标记:
立即学习“Python免费学习笔记(深入)”;
class Point:
__slots__ = ['x', 'y']
def __init__(self, x, y):
object.__setattr__(self, 'x', x)
object.__setattr__(self, 'y', y)
object.__setattr__(self, '_frozen', True) # 标记已初始化
<pre class="brush:php;toolbar:false;">def __setattr__(self, name, value):
if getattr(self, '_frozen', False):
raise TypeError(f"cannot modify '{name}' on immutable instance")
object.__setattr__(self, name, value)- 必须用
object.__setattr__绕过自定义逻辑,否则_frozen根本设不进去 - 不能依赖
hasattr(self, '_frozen')判断,因为首次访问可能触发__getattr__或报错;getattr(..., False)更安全 - 这种方案对继承不友好——子类若重写
__init__忘记设_frozen,就等于没锁住
用 dataclasses.field + frozen=True 更可靠
Python 3.7+ 的 @dataclass(frozen=True) 是更稳妥的选择:它自动处理 __slots__、__setattr__、__hash__,还内置对默认工厂、类型检查的支持。
但要注意它默认只冻结顶层字段,不递归冻结嵌套可变对象:
from dataclasses import dataclass, field <p>@dataclass(frozen=True) class Config: name: str tags: tuple = field(default_factory=tuple) # ✅ 正确:用 tuple 替代 list</p><h1>tags: list = field(default_factory=list) # ❌ 危险:list 可被原地修改</h1><p>
- 字段类型必须选不可变类型:
tuple、frozenset、str、int等;list和dict必须显式转为tuple或封装成MappingProxyType - 若字段有默认值且是可变对象(如
list),必须用default_factory,否则所有实例共享同一份引用 -
frozen=True会禁用__dict__,所以不能动态加属性,这对内存和性能是加分项
嵌套可变对象怎么安全封装
即使外层类不可变,如果字段本身是 list 或 dict,用户仍可通过 obj.items.append(...) 破坏数据一致性。必须切断原地修改路径。
常见封装方式:
- 用
tuple(items)替代list:适合只读序列,但不支持索引赋值(本来也不该支持) - 用
frozenset(items)替代set:去重且不可变 - 用
types.MappingProxyType(dict_obj)包装字典:提供只读视图,底层 dict 若被外部改,视图也会变——所以必须确保原始 dict 不被泄露 - 若需深度冻结(如嵌套 dict/list),需手动递归转换,或借助第三方库如
pyrsistent或immutables
最容易忽略的一点:构造函数里别直接接收可变对象并存为字段。应该在 __post_init__ 中做转换:
@dataclass(frozen=True)
class Report:
raw_data: list
def __post_init__(self):
# 强制转为 tuple,切断外部 list 的影响
object.__setattr__(self, 'raw_data', tuple(self.raw_data))
不可变类真正的难点不在语法层面,而在于你是否真的掌控了所有字段的生命周期——尤其是那些从外部传入的、看似无害的 list 或 dict。一不留神,它们就成了漏洞入口。


















