Python dataclass 如何让 post_init 里修改默认值

__post_init__ 可安全修改实例字段初始值,需确保字段参与初始化且未冻结;推荐用 field(init=False) 声明派生字段并在 __post_init__ 中计算赋值。

dataclass 中,__post_init__ 不能直接“修改默认值”本身(因为默认值在类定义时已固化),但可以安全地**修改实例字段的初始值**——前提是这些字段被正确标记为参与初始化,并且未被冻结。

确保字段参与初始化且可写

如果字段设置了 default=...default_factory=...,它默认参与 __init__;但如果设了 init=False,它就不会出现在参数中,__post_init__ 里也无法通过参数方式“重新赋默认值”,只能手动赋值:

  • 想在 __post_init__ 中设置/覆盖某个字段,该字段必须 不设 init=False(或显式设 init=True
  • 类不能设 frozen=True,否则所有字段变为只读,赋值会报 FrozenInstanceError
  • 若字段依赖其他字段计算,推荐用 field(init=False) + 在 __post_init__ 中赋值(这是标准做法)

field(init=False) 声明派生字段

这是最常见也最推荐的方式:把不希望用户传入、但需根据其他字段动态生成的字段,声明为 init=False,然后在 __post_init__ 中计算并赋值:

from dataclasses import dataclass, field

@dataclass class Product: name: str price: float tax_rate: float = 0.1

total 是派生字段,不由用户传入

total: float = field(init=False)

def __post_init__(self):
    self.total = self.price * (1 + self.tax_rate)

这样创建实例时只需传 namepriceProduct("book", 100.0)total 自动算出为 110.0

覆盖用户传入的值(谨慎使用)

如果字段允许用户传值,但你想在 __post_init__ 中强制修正(比如归一化、补缺、校验后调整),可以直接赋值:

@dataclass
class User:
    name: str
    email: str = ""
    age: int = 0
def __post_init__(self):
    # 如果没传 email,用默认格式生成
    if not self.email:
        self.email = f"{self.name.lower()}@example.com"
    # 强制 age ≥ 0
    if self.age < 0:
        self.age = 0

注意:这种覆盖会静默改变用户输入,建议搭配文档或类型提示说明行为。

避免常见错误

  • 别在 __post_init__ 里给 init=False 字段设 default=... —— 它不会生效,因为 default 只影响初始化参数逻辑
  • 不要在 __post_init__ 中调用 self.__dict__.update(...) 来批量赋值,易绕过字段逻辑,且与 dataclass 设计意图不符
  • 如果用了 __slots__,确保所有 field(init=False) 字段都包含在 __slots__ 中,否则赋值会失败