笔记 · JOURNAL

Python @dataclass 使用指南

适用版本:Python 3.7+(dataclasses 为标准库;3.10+ 部分特性增强)


1. 为什么需要 dataclass

在 Python 中,我们经常需要「只存数据、逻辑很少」的类,例如配置项、API 响应、数据库记录。手写样板代码很繁琐:

class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x!r}, y={self.y!r})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

@dataclass 装饰器根据类型注解自动生成 __init____repr____eq__ 等方法,让你专注于字段定义本身。

适合场景:

  • 配置对象(如 ML 训练的 ModelConfig / TrainConfig
  • 数据传输对象(DTO)
  • 结构化返回值
  • 临时聚合多个相关字段

不太适合:

  • 复杂业务逻辑、大量方法的领域模型
  • 需要严格运行时校验的场景(考虑 Pydantic)
  • 性能极度敏感的热路径(普通 dataclass 足够快,但 NamedTuple 有时更省内存)

2. 快速入门

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)           # Point(x=1.0, y=2.0)
print(p.x, p.y)    # 1.0 2.0
print(p == Point(1.0, 2.0))  # True

等价于手写 __init____repr____eq__,但代码只有 4 行。

带默认值的字段

@dataclass
class User:
    name: str
    age: int = 0
    active: bool = True

u = User("Alice")
# User(name='Alice', age=0, active=True)

规则: 没有默认值的字段必须放在有默认值的字段之前

# ❌ 错误:SyntaxError
@dataclass
class Bad:
    age: int = 0
    name: str

3. 核心参数详解

@dataclass 装饰器本身接受多个参数,控制生成哪些方法:

from dataclasses import dataclass

@dataclass(
    init=True,       # 生成 __init__(默认 True)
    repr=True,       # 生成 __repr__(默认 True)
    eq=True,         # 生成 __eq__(默认 True)
    order=False,     # 是否生成 __lt__ 等比较方法(默认 False)
    unsafe_hash=False,  # 是否生成 __hash__(默认 False)
    frozen=False,    # 是否不可变(默认 False)
    slots=False,     # Python 3.10+:是否使用 __slots__(默认 False)
    kw_only=False,   # Python 3.10+:字段是否仅关键字传参(默认 False)
)
class Example:
    ...
参数说明
init自动生成构造函数
repr自动生成可读字符串表示
eq按字段值逐字段比较相等性
order生成 <<=>>=(需字段可比较)
unsafe_hash生成 __hash__;若 eq=True 且未 frozen,可能不安全
frozen实例创建后不可修改字段(类似 namedtuple 的可变版反面)
slots使用 __slots__,减少内存、禁止动态添加属性
kw_only所有字段必须通过关键字传递(3.10+)

查看生成的源码

from dataclasses import dataclass
import inspect

@dataclass
class Point:
    x: float
    y: float

print(inspect.getsource(Point.__init__))

4. field() 高级用法

field() 用于对单个字段做细粒度控制:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Team:
    name: str
    members: List[str] = field(default_factory=list)
    _id: int = field(init=False, repr=False, default=0)
    score: float = field(default=0.0, compare=False)

常用 field() 参数

参数说明
default字段默认值(不可变对象)
default_factory默认值工厂(可变对象必须用此方式)
init是否出现在 __init__ 中(默认 True)
repr是否出现在 __repr__ 中(默认 True)
compare是否参与 __eq__ / 排序比较(默认 True)
hash是否参与 __hash__(默认 None,跟随 compare)
metadata附加元数据,供框架读取

init=False:派生字段

适合「由其他字段计算得出、不需要用户传入」的字段:

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)

    def __post_init__(self):
        self.area = self.width * self.height

r = Rectangle(3, 4)
print(r.area)  # 12

5. 默认值与可变对象陷阱

❌ 错误:可变默认值

@dataclass
class Bad:
    items: list = []  # 危险!所有实例共享同一个 list

✅ 正确:使用 default_factory

@dataclass
class Good:
    items: list = field(default_factory=list)
    tags: set = field(default_factory=set)
    meta: dict = field(default_factory=dict)

default_factory 必须是无参 callable,每次创建实例时调用。


6. __post_init__ 初始化后处理

__init__ 由 dataclass 自动生成,如需额外校验或计算,使用 __post_init__

@dataclass
class Email:
    address: str

    def __post_init__(self):
        if "@" not in self.address:
            raise ValueError(f"Invalid email: {self.address}")
        self.address = self.address.lower()

常见用途:

  • 参数校验
  • 计算派生字段
  • 规范化输入(转小写、去空格)
  • 调用 object.__setattr__ 修改 frozen 实例(见下文)

7. 继承与组合

dataclass 可以继承,但需注意默认值顺序规则仍然适用。

@dataclass
class Animal:
    name: str

@dataclass
class Dog(Animal):
    breed: str = "mixed"

d = Dog("Buddy", breed="corgi")
# Dog(name='Buddy', breed='corgi')

子类新增的无默认值字段仍须在有默认值字段之前。多层继承时,建议:

  • 父类定义公共字段
  • 子类扩展专用字段
  • 复杂层级考虑组合而非深层继承
@dataclass
class TrainConfig:
    batch_size: int = 32
    learning_rate: float = 3e-4

@dataclass
class ExperimentConfig:
    model_name: str
    train: TrainConfig = field(default_factory=TrainConfig)

8. 与 dict / JSON 互转

dataclass 本身不提供序列化,但很容易与标准库配合。

转为 dict

from dataclasses import asdict, astuple

@dataclass
class Config:
    lr: float = 1e-3
    steps: int = 1000

c = Config()
print(asdict(c))   # {'lr': 0.001, 'steps': 1000}
print(astuple(c))  # (0.001, 1000)

从 dict 还原

from dataclasses import dataclass

@dataclass
class Config:
    lr: float = 1e-3
    steps: int = 1000

data = {"lr": 0.01, "steps": 500, "extra": "ignored"}
valid = {f.name for f in Config.__dataclass_fields__.values()}
cfg = Config(**{k: v for k, v in data.items() if k in valid})

JSON 读写

import json
from dataclasses import dataclass, asdict

@dataclass
class Config:
    lr: float = 1e-3
    steps: int = 1000

# 写入
with open("config.json", "w") as f:
    json.dump(asdict(Config()), f, indent=2)

# 读取
with open("config.json") as f:
    cfg = Config(**json.load(f))

嵌套 dataclass 的 JSON

asdict() 会递归转换嵌套 dataclass;反向还原需手动处理或使用第三方库(如 dacitecattrs)。


9. 不可变 dataclass(frozen)

@dataclass(frozen=True)
class ImmutablePoint:
    x: float
    y: float

p = ImmutablePoint(1, 2)
# p.x = 3  # FrozenInstanceError

优点:

  • 实例可作为 dict 的 key 或 set 的元素(自动有 __hash__
  • 线程更安全
  • 语义清晰:配置一旦创建不应被修改

__post_init__ 中修改 frozen 实例:

@dataclass(frozen=True)
class Range:
    start: int
    end: int

    def __post_init__(self):
        if self.start > self.end:
            raise ValueError("start must <= end")
        # frozen 实例不能 self.x = ...,要用 object.__setattr__
        object.__setattr__(self, "start", min(self.start, self.end))

10. 排序、比较与哈希

启用排序

@dataclass(order=True)
class Score:
    value: int
    name: str = field(compare=False)  # 不参与比较

Score(10, "a") < Score(20, "b")  # True,只比较 value

字段按定义顺序依次比较。

哈希

配置__hash__ 行为
frozen=True自动生成
eq=True, frozen=False默认不可哈希(__hash__ = None
unsafe_hash=True强制生成(可变对象作 key 有风险)

11. dataclass vs NamedTuple vs dict vs Pydantic

特性dataclassNamedTupledictPydantic BaseModel
可变默认是默认可配置
类型注解
运行时校验
内存中等较小较大较大
JSON 集成需手动需手动原生原生
IDE 补全
学习成本最低

选择建议:

  • 内部配置 / 简单 DTOdataclass
  • 不可变、轻量、可当 keyNamedTuplefrozen dataclass
  • API 边界、用户输入校验 → Pydantic
  • 完全动态、字段不固定 → dict

12. 类型注解与 IDE 支持

dataclass 依赖类型注解生成方法签名,应始终标注类型:

from dataclasses import dataclass
from typing import Optional

@dataclass
class Job:
    id: int
    title: str
    description: Optional[str] = None

Python 3.9+ 可直接写 list[str]dict[str, int];更早版本用 typing.List 等。

ClassVar:类变量而非实例字段

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class Counter:
    DEFAULT: ClassVar[int] = 0   # 不会出现在 __init__ 中
    value: int = 0

13. 常见模式与最佳实践

模式 1:配置对象(ML / Web 项目常见)

@dataclass
class ModelConfig:
    hidden_size: int = 384
    num_layers: int = 6
    dropout: float = 0.1

@dataclass
class TrainConfig:
    batch_size: int = 32
    lr: float = 3e-4
    max_steps: int = 10_000

模式 2:工厂函数替代多参数

# ❌ 参数过多
def train(lr, batch_size, hidden_size, num_layers, ...):
    ...

# ✅ 配置对象
def train(cfg: TrainConfig, model_cfg: ModelConfig):
    ...

模式 3:replace() 做不可变更新

from dataclasses import dataclass, replace

@dataclass
class Config:
    lr: float = 1e-3
    steps: int = 1000

base = Config()
fast = replace(base, lr=1e-2)  # 新实例,base 不变

frozen=True 的 dataclass 尤其有用。

模式 4:字段 introspection

from dataclasses import fields, is_dataclass

@dataclass
class Config:
    lr: float = 1e-3

for f in fields(Config):
    print(f.name, f.type, f.default)

print(is_dataclass(Config))  # True

最佳实践清单

  1. 可变默认值一律用 field(default_factory=...)
  2. 配置类优先 dataclass,需要校验再上 Pydantic
  3. 不想被意外修改的配置用 frozen=True
  4. 序列化用 asdict(),反序列化过滤未知键
  5. 复杂初始化逻辑放 __post_init__,保持字段声明简洁
  6. 大型项目里按职责拆分多个小 dataclass,避免「上帝配置类」

14. 完整实战示例

以下示例综合:默认值、校验、JSON、replace、嵌套配置。

from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field, replace
from pathlib import Path
from typing import List


@dataclass
class OptimizerConfig:
    lr: float = 3e-4
    weight_decay: float = 0.1

    def __post_init__(self):
        if self.lr <= 0:
            raise ValueError("lr must be positive")


@dataclass
class ModelConfig:
    vocab_size: int = 4096
    max_seq_len: int = 128
    d_model: int = 384
    n_layers: int = 6
    n_heads: int = 6
    dropout: float = 0.1

    def __post_init__(self):
        if self.d_model % self.n_heads != 0:
            raise ValueError("d_model must be divisible by n_heads")


@dataclass
class Experiment:
    name: str
    model: ModelConfig = field(default_factory=ModelConfig)
    optimizer: OptimizerConfig = field(default_factory=OptimizerConfig)
    tags: List[str] = field(default_factory=list)

    def save(self, path: str | Path) -> None:
        Path(path).write_text(json.dumps(asdict(self), indent=2), encoding="utf-8")

    @classmethod
    def load(cls, path: str | Path) -> Experiment:
        raw = json.loads(Path(path).read_text(encoding="utf-8"))
        return cls(
            name=raw["name"],
            model=ModelConfig(**raw["model"]),
            optimizer=OptimizerConfig(**raw["optimizer"]),
            tags=raw.get("tags", []),
        )


if __name__ == "__main__":
    exp = Experiment("baseline", tags=["guppylm", "demo"])
    exp.save("experiment.json")

    loaded = Experiment.load("experiment.json")
    finetune = replace(
        loaded,
        name="finetune-lr",
        optimizer=replace(loaded.optimizer, lr=1e-4),
    )
    print(finetune)

15. 常见问题 FAQ

Q1:@dataclass 和普通类有什么本质区别?

没有运行时魔法,只是代码生成器:装饰器在类定义时自动生成方法。本质上仍是普通 Python 类。

Q2:能否不用类型注解?

可以写 @dataclass class Foo: x: Any,但失去 IDE 和静态检查的好处。没有注解的字段会被当作 Any

Q3:dataclass 线程安全吗?

普通可变 dataclass 与其他可变对象一样,多线程同时改字段需要自行加锁。frozen=True 的实例线程安全(只读)。

Q4:__init__ 能手动覆盖吗?

可以,但覆盖后 dataclass 不会再自动生成 __init__,需自己维护。一般改用 __post_init__

Q5:如何排除某些字段不参与 repr

field(repr=False),例如内部 id、缓存字段。

Q6:Python 3.10+ 的 slots=True 值得开吗?

大量实例时省内存、略提速;调试时不能随意 obj.new_attr = 1。按需开启。

Q7:和 attrs 库的关系?

attrs 是第三方先驱,功能更丰富;标准库 dataclassesattrs 启发,API 相似但更简单。新项目若无 attrs 依赖,优先标准库。


速查表

from dataclasses import (
    dataclass,      # 装饰器
    field,          # 字段配置
    fields,         # 获取字段元信息
    asdict,         # 实例 → dict
    astuple,        # 实例 → tuple
    replace,        # 复制并修改部分字段
    is_dataclass,   # 类型判断
    make_dataclass, # 动态创建 dataclass
)
# 最小可用模板
from dataclasses import dataclass, field

@dataclass
class MyConfig:
    name: str
    count: int = 0
    items: list = field(default_factory=list)

    def __post_init__(self):
        if self.count < 0:
            raise ValueError("count >= 0")

延伸阅读


文档版本:2026-06 | 示例代码均在 Python 3.10+ 验证通过