直接给实例赋值会失败是因为通过__slots__ + __init__ + __setattr__组合或dataclasses.frozen=true封死了所有写入路径,拦截初始化后的属性赋值并抛出异常。

为什么直接给实例赋值会失败?
Python 默认不阻止对实例属性的修改,所谓“不可变”必须靠编码约定或机制约束。常见误区是以为 __slots__ 或 @property 单独就能实现不可变——其实它们只控制属性存在性或读写权限,不阻止 __dict__ 动态赋值或绕过访问器的操作。
用 __slots__ + __init__ + __setattr__ 组合封死写入路径
这是最常用、兼容性好、且真正生效的方式。核心是拦截所有属性赋值动作,并只允许在初始化阶段写入。
示例:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
object.__setattr__(self, "x", x)
object.__setattr__(self, "y", y)
def __setattr__(self, name, value):
raise TypeError(f"'{self.__class__.__name__}' is immutable")
关键点:
-
__slots__禁止动态添加属性,也防止生成__dict__ - 初始化时必须用
object.__setattr__,否则会被刚定义的__setattr__拦截 - 自定义
__setattr__在初始化后生效,确保后续任何赋值都报错 - 如果类有父类,需确认父类是否也遵循同样逻辑,否则可能被父类的
__setattr__绕过
用 dataclasses 的 frozen=True 快速实现(Python 3.7+)
这是最简洁的方案,但底层仍依赖 __slots__ 和重写的 __setattr__,且有隐含限制。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
示例:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
注意:
- 一旦启用
frozen=True,连self.x = ...在方法内部也会失败(会抛出FrozenInstanceError) - 若字段类型是可变对象(如
list),实例仍是逻辑上可变的——frozen只冻结属性引用,不冻结引用对象的内容 - 不能与
__slots__ = ...手动混用,dataclass会自动处理 - 继承自 frozen dataclass 的子类,必须也声明
frozen=True,否则会破坏不可变性
不可变类常被忽略的边界问题
真正的不可变性不只是“不能改属性”,还涉及对象图(object graph)的完整性。
容易踩的坑:
- 字段值是
list、dict或自定义可变类时,虽然实例本身不能换引用,但能通过obj.items.append(...)修改内容 - 使用
copy.copy()得到的是浅拷贝,新实例字段仍指向原可变对象 - 若类实现了
__getstate__/__setstate__,反序列化过程可能绕过__init__和__setattr__,需额外防护 - 元类或
__new__中未调用父类__new__,可能导致__slots__失效
复杂点不在语法,而在你是否清楚每个字段的生命周期和所有权归属。哪怕一个 datetime 字段,若它来自外部且未深拷贝,也可能成为漏洞入口。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










