最稳妥的只读属性方案是用 @property 配合 __init__ 中赋值,不定义 setter;它拦截常规赋值、兼容类型检查,但不禁止直接操作 _x;真正封死需结合 __slots__ 与自定义 __setattr__ 或 dataclass(frozen=True)。

用 @property 定义只读属性最稳妥
直接在类中给 self._x = value 赋值,再配一个不带 setter 的 @property,是 Python 里最常用也最符合预期的只读方案。它不阻止你改底层变量(比如 obj._x),但能拦住 obj.x = ... 这种常规赋值,且 IDE 和类型检查器(如 mypy)都能识别。
常见错误是只写 @property 却没加 @xxx.setter,结果误以为完全不可修改——其实绕过 property 直接操作下划线字段仍可行。真正想封死,得配合 __setattr__ 或 __slots__。
-
@property方法里别做重计算或 I/O,否则每次访问都触发,性能易翻车 - 如果属性依赖初始化参数,确保在
__init__中完成赋值,不要在@property里懒加载并缓存(除非明确需要) - 类型注解建议加上:例如
x: int放在@property方法签名后,mypy 才能校验调用方是否当它是只读
用 __slots__ + __setattr__ 拦截所有赋值
当必须禁止任何字段被动态新增或修改(包括私有字段),__slots__ 配合自定义 __setattr__ 是更硬核的做法。它能在运行时抛出 AttributeError,比纯 @property 更彻底。
容易踩的坑是忘了在 __setattr__ 里放行 __init__ 阶段的赋值——否则对象根本构造不出来。典型做法是用标志位或检查 self.__dict__ 是否为空来区分初始化和后续修改。
立即学习“Python免费学习笔记(深入)”;
- 启用
__slots__后,实例不再有__dict__,所以不能靠getattr(obj, 'x', default)动态查不存在的属性,得提前定义好 slot 名 -
__setattr__里用super().__setattr__(name, value)绕过拦截,仅限内部可信代码使用;对外一律 raise - 这种方案对继承敏感:子类若没声明
__slots__,父类的限制就失效了
用 dataclass(frozen=True) 快速获得不可变实例
如果你的类本质是数据容器(字段少、无复杂逻辑),@dataclass(frozen=True) 是最快捷的只读方案。它自动生成 __hash__、禁止所有字段修改,并在尝试赋值时立刻报 FrozenInstanceError。
注意它冻结的是整个实例,不是单个字段。哪怕你只想让 id 只读、name 可变,它也不支持——此时得退回 @property 或手动控制。
-
frozen=True会禁用__dict__,所以无法在运行时动态加属性(连obj._cache = ...都不行) - 如果字段默认值是可变对象(如
list),必须用default_factory,否则所有实例共享同一份引用 - 继承 frozen dataclass 时,子类也必须声明
frozen=True,否则父类字段可被绕过修改
为什么不用 __setitem__ 或描述符强行“锁死”?
有人试过用描述符(Descriptor)在 __set__ 里无条件 raise,或重写 __setitem__ 拦字典式访问——这些方法在特定场景(如模拟常量类)可能有用,但绝大多数时候是过度设计。
它们的问题在于:破坏 Python 的惯性认知。用户调用 obj.x = 1 时,期望得到明确的属性行为,而不是被描述符静默吞掉或抛出不相关的异常。而且这类方案往往漏掉 setattr(obj, 'x', 1) 这种反射调用,还得额外补 __setattr__,反而更难维护。
- 描述符的
__set__对obj.__dict__['x'] = 1无效,而这是很多框架(如 ORM、序列化库)实际用的底层操作 -
__setitem__只管obj['x'],对点号访问完全没影响,混淆语义 - 真正需要“绝对不可变”的场景极少,多数时候只需约定+工具链约束(如 mypy + pre-commit hook)就够了
真正难的不是实现只读,而是判断哪些字段该只读、哪些该开放、以及要不要为这点安全性增加维护成本。Python 的哲学是“我们都是 consenting adults”,所以多数项目用 @property 加文档说明,已经足够清晰。硬上 frozen=True 或 __setattr__ 前,先确认你的使用者真的会、且有必要去绕过那些防护。


















