
本文介绍一种简洁可靠的方法,使用 @dataclass(frozen=True) 结合自定义 __call__ 方法,实现真正不可变、可嵌套更新、语法优雅的函数式数据结构,如 person(name='Alice Smith')(occupation={'salary': 95000})。
本文介绍一种简洁可靠的方法,使用 `@dataclass(frozen=true)` 结合自定义 `__call__` 方法,实现真正不可变、可嵌套更新、语法优雅的函数式数据结构,如 `person(name='alice smith')(occupation={'salary': 95000})`。
在构建领域模型或配置对象时,我们常需要兼顾不可变性(immutability)与易用性(ergonomics):既要防止意外修改状态,又要让字段更新(尤其是嵌套字段)像函数调用一样直观流畅。Python 的 dataclasses.replace() 天然支持顶层字段替换,但默认不支持对嵌套冻结数据类(如 Occupation)进行“深层合并式”更新。直接在 frozen=True 类中定义 @property + @setter 会破坏不可变契约(因为 object.__setattr__ 绕过了冻结检查,且逻辑难以维护),因此必须采用纯构造式(constructive)方案。
核心思路是:让每个冻结数据类自身具备“生成新实例”的能力,并在父类 __call__ 中智能处理嵌套子对象的重建。以下是推荐实现:
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Occupation:
title: str
salary: int
def __call__(self, **kwargs):
return replace(self, **kwargs)
@dataclass(frozen=True)
class Person:
name: str
occupation: Occupation
def __call__(self, **kwargs):
# 若传入 occupation 参数,先将其转换为新的 Occupation 实例
if 'occupation' in kwargs:
occ_arg = kwargs['occupation']
if isinstance(occ_arg, dict):
# 深层合并:用当前 occupation 作为基础,覆盖指定字段
new_occ = self.occupation(**occ_arg)
elif isinstance(occ_arg, tuple):
new_occ = self.occupation(*occ_arg)
elif isinstance(occ_arg, Occupation):
new_occ = occ_arg
else:
raise TypeError(f"Unsupported occupation type: {type(occ_arg)}")
kwargs['occupation'] = new_occ
return replace(self, **kwargs)使用示例如下:
# 初始化
a0 = Person(name='John', occupation=Occupation(title='junior', salary=30))
a1 = a0(name=a0.name + ' Smith') # → Person(name='John Smith', ...)
a2 = a1(occupation={'title': 'senior'}) # → 新 Occupation(title='senior', salary=30)
a3 = a2(occupation={'salary': 50}) # → 新 Occupation(title='senior', salary=50)✅ 优势说明:
立即学习“Python免费学习笔记(深入)”;
- 完全不可变:所有实例均为 frozen=True,无 setter、无隐藏状态;
- 嵌套即扁平:a(occupation={'salary': X}) 语义清晰,无需手动解构再构造 Occupation;
- 类型安全:Occupation.__call__ 本身也返回 Occupation,支持 IDE 推导和 mypy 检查;
- 可扩展性强:任意嵌套层级(如 Company → Department → Person → Occupation)均可沿用相同模式。
⚠️ 注意事项:
- 字段名拼写需严格一致(如原文中的 sallary 应为 salary,否则 **kwargs 会抛出 TypeError);
- 不支持“部分字典合并”(如 {'title': 'lead'} 不会保留原 salary?——它会!因为 self.occupation(**occ_arg) 是以当前实例为模板,仅覆盖传入键,其余字段自动继承);
- 若需更高级的合并策略(如递归合并嵌套 dict),可将 Occupation.__call__ 替换为自定义 merge_with 方法。
最后,若项目中此类模式大量出现,可通过元类或 __post_init__ 阶段动态注入 __call__,或借助第三方库如 pydantic v2(其 model_copy(update=...) 更强大)或 immutablecollections,但对轻量级场景,上述纯 dataclass 方案已足够稳健、透明且零依赖。


















