← 回到手记
代码

dataclass:别再手写一堆 __init__ 了

用标准库 @dataclass 自动生成 __init__/__repr__/__eq__;可变字段默认值请用 field(default_factory=...),别再写 []。

要解决的问题

写 Python 后端时,经常要定义「就几个字段」的数据类:购物车、配置片段、API 请求体、任务队列里的一条消息。很多人会手写 __init__、__repr__、__eq__,字段一多就重复,改一个字段还要改三处,特别容易漏。更糟的是,为了省事在类属性或函数参数里写 items = [],几个实例会悄悄共用同一份列表——表面上各自 add,调试时却发现「别人」的数据也进来了。

标准库模块 dataclasses 从 Python 3.7 起可用(见 PEP 557)。加一个 @dataclass,就能按类型注解生成 __init__、__repr__、__eq__ 等常用方法。可变字段的默认值要用 field(default_factory=...),不要直接写 [] / {}。本文示例已在本机 Python 3.13.5 验证;3.12 及以上写法一致,老一点的 3.7–3.11 也可用同一套核心 API(个别参数如 slots、kw_only 是后续版本才加的)。

核心概念

日常先记住这几条:

  • @dataclass:装饰器扫描带类型注解的字段,按定义顺序生成方法。默认会生成 __init__、__repr__、__eq__(见官方文档「Module contents」)。类上已有同名方法时,对应开关会被忽略或按文档规则处理。
  • 字段注解:name: str、quantity: int = 0。有默认值的字段必须排在无默认值字段后面,否则构造函数签名不合法,会 TypeError。
  • field(default_factory=...):default_factory 必须是零参数可调用对象;需要默认值时调用它,得到新对象。官方例子:mylist: list[int] = field(default_factory=list)。不能同时写 default 和 default_factory。
  • 可变默认值:普通类把可变对象放在类属性上,多实例会共享。@dataclass 若检测到不可哈希的默认值(常用来近似「可变」),会直接 ValueError,逼你改用 default_factory(3.11 起按「不可哈希」判断,见文档「Mutable default values」)。
  • asdict / replace:需要转成字典、或基于现有实例改个别字段得到新对象时可用;嵌套结构与浅拷贝注意点以官方文档为准,本文不展开。

完整说明:dataclasses — Data Classes(Python 3.12)。设计动机与目标见 PEP 557。

示例代码

手写版和 dataclass 版对比(本地已跑通):

from dataclasses import dataclass, field

# 手写:啰嗦,还容易漏 __repr__ / __eq__
class CartManual:
    def __init__(self, user: str, items: list[str] | None = None):
        self.user = user
        # 注意:默认不要写 items=[],那是所有调用共享的同一列表
        self.items = [] if items is None else items

    def add(self, name: str) -> None:
        self.items.append(name)

@dataclass
class Cart:
    user: str
    items: list[str] = field(default_factory=list)

    def add(self, name: str) -> None:
        self.items.append(name)

a = Cart("alice")
b = Cart("bob")
a.add("apple")
assert a.items == ["apple"]
assert b.items == []          # 两个实例列表互不影响
assert a.items is not b.items
assert a == Cart("alice", ["apple"])  # 自动生成的 __eq__
print(a)  # Cart(user='alice', items=['apple'])

官方文档「Mutable default values」一节用普通类演示了共享类属性列表的问题(大意是:o1 和 o2 的 x 其实是同一个对象)。若在 dataclass 里写成 x: list = [],装饰器会拦住,本机报错类似:

ValueError: mutable default <class 'list'> for field x is not allowed: use default_factory

正确写法就是上面的 field(default_factory=list)。函数参数同样不要写 def f(xs=[]),应写成 xs: list[str] | None = None,在函数体内再 xs = [] if xs is None else xs。

需要把实例变成可 JSON 序列化的字典时,可以用 dataclasses.asdict(a);只改一个字段得到新实例,可以用 dataclasses.replace(a, user="carol")。二者都会按文档做递归或重新走 __init__,用前先读一眼对应小节。

常见误区

  1. items: list = [] 或类属性 x = []
    多实例会共享同一列表,改一个等于改全家。dataclass 多数情况会拦住不可哈希默认值;仍应养成 default_factory 的习惯。

  2. 有默认值的字段写在无默认值字段前面
    会 TypeError。先写必填,再写带默认值的。继承时父类与子类字段合并后也要满足这一规则。

  3. 以为 @dataclass 会「冻结」对象
    默认实例仍可变。需要近似只读时用 @dataclass(frozen=True)(赋值会触发 FrozenInstanceError),细节见官方「Frozen instances」。注意 frozen 与 eq 组合还会影响能否自动生成 __hash__。

  4. 用 dataclass 硬扛复杂行为
    需要依赖其他字段做二次初始化时,可以写 __post_init__;复杂继承、描述符字段、InitVar 等进阶用法请直接查文档,不要凭感觉拼。字段很多、行为很重的领域模型,也可以继续用手写类或 Pydantic 等方案,dataclass 不是唯一答案。

小结

「就几个字段」的数据容器,优先 @dataclass:少写样板代码,__repr__ / __eq__ 还不容易漏。可变字段的默认值一律 field(default_factory=...),别在类属性或函数默认参数里塞 [] / {}。本文按 Python 3.12 dataclasses 文档 与 PEP 557 核对;正式项目以你当前解释器版本的官方文档为准。

参考资料

  1. dataclasses — Data Classes — Python 3.12 文档
  2. PEP 557 – Data Classes
  3. dataclasses — Data Classes — 最新稳定版文档
✳

谢谢你读到这里。