Python frozen dataclass 里怎么保存派生字段
来源:17golang原创
时间:2026-09-08 09:07:22 213浏览 收藏
在订单行、配置快照这类对象里,total、cache_key 之类的字段通常不是调用方直接传入,而是由输入字段计算出来。使用 @dataclass(frozen=True) 后,普通的 self.total = ... 会触发 FrozenInstanceError。稳妥的写法是把派生字段声明为 init=False,在 __post_init__ 中用 object.__setattr__ 完成一次初始化;默认列表则交给 default_factory,不要把可变对象写成类级共享默认值。
核心做法:输入字段参与生成构造参数,派生字段用field(init=False)排除出构造签名;frozen=True下只在__post_init__里通过object.__setattr__写入一次。
__post_init__适合计算依赖多个输入的字段。default_factory每次构造时创建独立默认值,避免列表互相污染。frozen只限制属性重新绑定,不会自动冻结列表内部内容。
为什么派生字段要放进 __post_init__
生成的 __init__ 只负责接收真实输入。把派生值标记为 init=False,可以让调用方不能绕过计算逻辑直接传入;__post_init__ 又紧跟在生成构造方法之后,适合集中处理字段之间的关系。
下面的例子模拟订单行:税率是构造阶段的临时输入,含税金额是派生字段。frozen=True 时要用 object.__setattr__,这只解决初始化写入,不代表后续代码可以随意修改实例。
from dataclasses import InitVar, dataclass, field
from decimal import Decimal
@dataclass(frozen=True)
class OrderLine:
name: str
base_price: Decimal
quantity: int = 1
tax_rate: InitVar[Decimal] = Decimal("0.13")
total: Decimal = field(init=False)
def __post_init__(self, tax_rate: Decimal) -> None:
# 派生值不进入构造签名,只由输入字段统一计算
amount = self.base_price * self.quantity
total = amount * (Decimal("1") + tax_rate)
# frozen 实例禁止普通赋值,初始化阶段用底层写入
object.__setattr__(self, "total", total)
line = OrderLine("键盘", Decimal("199"), quantity=2)
print(line.total) # 449.74
这里的 tax_rate 是 InitVar,只参与生成的 __init__ 和 __post_init__,不会成为实例字段。total 则是真正的字段,因此可被 fields() 和序列化逻辑看到。

__post_init__ 计算 total,冻结边界内只通过 object.__setattr__ 完成初始化写入。default_factory 和字段顺序怎么一起检查
默认值和字段位置会共同决定生成的 __init__。没有默认值的字段必须放在有默认值的字段前面,这条规则也适用于继承后的字段合并。对于列表、集合等容器,写成 tags: list[str] = [] 会让多个实例共享同一个对象;应改用零参数工厂。
from dataclasses import InitVar, dataclass, field, fields
@dataclass(frozen=True)
class Job:
name: str
quantity: int = 1
tags: list[str] = field(default_factory=list)
config: InitVar[dict] = None
def __post_init__(self, config: dict | None) -> None:
# config 只用于初始化判断,不保存为实例字段
if config and config.get("tag"):
self.tags.append(config["tag"])
first = Job("build", config={"tag": "release"})
second = Job("test")
print(first.tags, second.tags) # ['release'] []
print([item.name for item in fields(Job)]) # name, quantity, tags
示例中的 tags 是列表,所以 frozen=True 并不能阻止 append;它冻结的是属性绑定,而不是对象内部的可变内容。如果目标是更强的不可变边界,可以把类型改成 tuple[str, ...],并用 default_factory=tuple。
| 声明 | 进入 __init__ | 出现在 fields() | 适用边界 |
|---|---|---|---|
field(init=False) | 否 | 是 | 保存派生字段 |
default_factory=list | 可带默认值 | 是 | 每个实例独立容器 |
InitVar[dict] | 是 | 否 | 只在构造阶段使用的输入 |
frozen=True | 不改变参数规则 | 不改变字段集合 | 阻止普通属性重新赋值 |
生成签名不稳定时,先按这三个边界排查
第一,看必填字段是否误放在默认字段之后;第二,看 InitVar 是否只是临时依赖,若需要持久化就应改成普通字段;第三,看派生字段是否错误地保留了 init=True。需要检查真实字段集合时,用 fields(),不要把类的注解字典当成最终 dataclass 结构。
如果类使用继承,基类 dataclass 字段会参与最终顺序,子类同名字段还可能覆盖基类定义。生产代码可以把构造参数尽量改成关键字调用,减少字段新增或重排带来的调用方风险。

__init__ 参数,default_factory=list 提供独立容器,InitVar 只连接初始化链路而不进入 fields()。常见问题
frozen=True 下能不能在 __post_init__ 里直接赋值?
不能。直接写 self.total = value 会触发冻结异常,应使用 object.__setattr__ 完成派生字段的初始化。
default_factory 每次都会重新调用吗?
当该字段没有显式传值时,生成的构造方法会为当前实例调用工厂,因此适合创建独立列表、集合或元组。
InitVar 适合保存数据库连接吗?
它适合接收连接、配置等构造期依赖,再在 __post_init__ 中提取需要的值;连接对象本身不会自动成为实例字段。
派生字段应该用 property 还是 init=False?
需要序列化、比较或缓存结果时可用 init=False;必须始终随输入实时变化且不想保存副本时,通常更适合用只读 property。
-
264 收藏
-
264 收藏
-
488 收藏
-
文章 · python教程 | 4小时前 | 并发 · 日志 · Python教程 · 多进程 · 排障 · Python Fork multiprocessing 多进程日志 spawn QueueHandler QueueListener300 收藏
-
107 收藏
-
文章 · python教程 | 7小时前 | 异步编程 · Python教程 · 超时处理 · 任务取消 · Python asyncio CancelledError TaskGroup wait_for shield441 收藏
-
475 收藏
-
468 收藏
-
286 收藏
-
270 收藏
-
488 收藏
-
298 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习