dataclass 如何让 frozen=True 但允许在 post_init 修改

可以,但必须用 object.__setattr__ 绕过冻结检查;frozen=True 仅在 post_init 后生效,此时普通赋值触发 FrozenInstanceError,因 dataclass 已注入受限 __setattr__。

dataclass frozen=True 时 __post_init__ 能否修改字段?

可以,但必须用 object.__setattr__ 绕过冻结检查。dataclass 的 frozen=True 仅在实例创建完成后生效,而 __post_init__ 是初始化流程的最后一步、冻结尚未真正“锁死”对象——此时直接赋值仍会触发 FrozenInstanceError,因为普通 self.x = ... 走的是被重写的 __setattr__;必须退回到内置的、未被 dataclass 劫持的版本。

为什么 self.field = value__post_init__ 里报错?

因为 frozen=True 会让 dataclass 自动注入一个受限的 __setattr__,它在任何字段赋值时都检查是否已冻结。即使在 __post_init__ 中,该方法也已就位。错误信息是:FrozenInstanceError: cannot assign to field 'x'

  • object.__setattr__(self, 'field', value) 是唯一安全写法
  • 不能用 vars(self)['field'] = value —— 这绕过了 descriptor 协议,对有 @property 或自定义 __set__ 的字段无效
  • 不能在 __post_init__ 外部用 object.__setattr__ 修改,那真就冻结了

实际例子:计算派生字段并保持不可变

@dataclass(frozen=True)
class Point:
    x: float
    y: float
    distance_from_origin: float = field(init=False)
def __post_init__(self):
    # 允许计算并设置 init=False 字段
    object.__setattr__(self, 'distance_from_origin', (self.x**2 + self.y**2)**0.5)

注意:distance_from_origin 必须声明为 field(init=False),否则 dataclass 会在 __init__ 里尝试读它,导致缺失参数错误。所有在 __post_init__ 中要设的字段都得这样声明。

容易忽略的边界情况

如果字段类型是可变对象(比如 listdict),即使 frozen=True,你仍能原地修改其内容(如 self.items.append(...))。这不是 bug,而是 Python 对象模型的自然行为——frozen

冻结“绑定关系”,不冻结对象内部状态。若需彻底不可变,得配合 tuplefrozendict 或手动深拷贝加防御性复制。