DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
雨云;给项目一台云服务器
晚安云;香港 CN2 ¥15/月

cattrs 省略 dataclass 工厂默认值后,Union 读回成另一类型

cattrs 26.2.1 把 default_factory 字段误当唯一必填键,缺键时可能选错 Union 成员。类型已知时直接 structure 具体类;无标签字典本身有歧义,不能承诺自动恢复原类。

一个 dataclass 对象先由 cattrs 转成字典,再按 Union[D1, D2] 读回,原来的 D1 却静默变成 D2。D1 多出来的字段使用 default_factory,同时开启了 omit_if_default=True 时,应检查省略后还剩不剩足够的类型判别信息。

例如 D1 有 x 和缺省为空列表的 tags,D2 只有 x。D1 的空列表被省略后,输出只有 {'x': 1}。这份字典本身就不能证明原对象属于哪一种类型。

正式 26.2.1 会把工厂默认值误当作必填字段

当前默认联合判别器从 dataclass 的原始字段读取 .default。只设置 default_factory 时,原始 .default 仍是 MISSING,于是 tags 被当成 D1 的唯一必填键;输入没有这个键时,判别器便退到 D2。

正常处理这类默认值后,也不能承诺 {'x': 1} 自动选回 D1。缺少判别信息时,拒绝歧义比静默选错更合理。交换 Union 中两个类的顺序,并不能恢复已经省略的类型信息。

如果调用上下文已经确定目标是 D1,可以直接按具体类型读取。下面的例子不让默认 Union 判别器猜类型:

from dataclasses import dataclass, field
from cattrs import Converter

@dataclass
class D1:
    x: int
    tags: list[str] = field(default_factory=list)

@dataclass
class D2:
    x: int

converter = Converter(omit_if_default=True)
payload = converter.unstructure(D1(1))
restored = converter.structure(payload, D1)
assert isinstance(restored, D1)
assert restored.tags == []

这个入口只适用于类型已知的调用方。若消息确实允许 D1 或 D2,不能把所有缺少 tags 的旧消息都强制按 D1 处理;那会把原来合法的 D2 也改成 D1。

联合协议应有独立于默认字段的判别信息

需要长期保存或跨服务传递联合类型时,应明确类型标签或其他稳定判别规则,并让读写两端一起执行。新增标签涉及旧消息兼容和无标签数据的处理,不能只在一个读取函数里默认补值。一个会被省略的空列表,不适合承担稳定的类型标识。

也不要把 default_factory=list 改成共享列表默认值,或删除字段默认值强迫所有调用方传参。那会改变模型构造和实例隔离,却没有补足历史字典的类型信息。

验收应同时比较实际类、字段值和序列化格式,覆盖默认空列表、非空列表、D2、缺失标签及未知标签。只看 x == 1 会漏掉这次静默类型替换。对已经落库的无标签记录,保留原消息和可用上下文;没有可靠依据时应标为待确认,不能按当前默认规则批量重写类型。