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

特性 dataclass NamedTuple dict Pydantic 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+ 验证通过