DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

升级 Python 3.11 后,自定义 dataclass 默认值也报 mutable default

Python 3.11 起会拒绝自定义不可哈希字段默认值。找到定义失败的字段,用零参数 default_factory 保留对象语义,核对第三方库兼容性。

Python 3.10 能导入的模块,换到 3.11 后在 @dataclass 处报 mutable default ... is not allowed: use default_factory。报错对象未必是列表,也可能是版本号、配置对象或其他自定义类型。类尚未完成定义,修改实例化参数没有作用。

从 Python 3.11 起,dataclass 不再只拦截 list、dict 和 set 默认值,而是用不可哈希来近似识别可变默认值。这解释了为什么以前接受的自定义对象也会被拒绝,但不能据此断言它一定可变。

先找到定义失败的字段

记录实际解释器、依赖版本和 traceback 指向的类定义。若失败发生在第三方库导入时,要区分库自己的字段与项目调用方式,别只改调用它的那一行。

对于自己维护的模型,检查字段右侧是否直接创建了一个对象,以及这个类型的 __hash__。标准实现对普通实例字段检查的是默认值所属类的哈希属性;只给某个实例临时添加同名属性,不能改变这项规则。

若报错是 non-default argument ... follows default argument,则应检查继承后的构造参数顺序,处理路径见 dataclass 继承中的默认字段排序。它与这里的默认对象检查不同。

用工厂保留每个实例自己的默认对象

下面是自己拥有模型定义时的示例。Policy 含有实例数据,Job 没传 policy 时应创建独立配置:

from dataclasses import dataclass, field


@dataclass
class Policy:
    retries: int = 3
    labels: list[str] = field(default_factory=list)


@dataclass
class Job:
    policy: Policy = field(default_factory=Policy)


first = Job()
second = Job()
assert first.policy is not second.policy
assert first.policy.labels is not second.policy.labels

default_factory 接收零参数可调用对象,按需要创建默认值。要传构造参数,可以用零参数函数或 lambda,但须检查它实际创建什么。default_factory=lambda: shared_policy 仍然返回同一个共享对象,不能用来保证独立状态。

不要写成 default_factory=Policy(),那是提前调用后传入实例;也不要同时提供 default 和 default_factory。修改还要保留显式传入配置的原约定:工厂只负责缺省值,不能擅自复制或替换调用者提供的对象。

不通过哈希或 ClassVar 绕过字段含义

自定义不可哈希对象也可能在业务上不可变,哈希性只是这项检查的近似条件。需要针对对象语义选默认值,而不是看到错误就增加任意 __hash__,让可能变化的对象变成字典键或集合元素。

给外层 dataclass 加 unsafe_hash=True,与其字段默认对象所属类的哈希属性是两件事。它不是这条错误的通用修复,还会引入哈希与相等性维护要求。

ClassVar 只适合有意共享的类属性。把实例配置改为 ClassVar 会改变 dataclass 字段、构造参数及序列化等行为;不能把它作为不想写工厂时的替代。

第三方库的类定义失败,应核对该库支持的 Python 范围与已发布兼容版本,在独立环境调整完整依赖组合。不要直接编辑生产环境的 site-packages,也不要仅凭相似报错指定一个未经确认的“修复版本”。

修改后检查导入、默认构造、显式参数和两个实例之间的状态隔离;模型对外输出也应保持预期。保存原依赖锁文件和模型定义,通过项目原启动流程加载新代码。回退解释器或库版本前,同样要检查其他依赖约束,不能把降级当作无影响操作。