Python @dataclass 使用指南
- 笔记
- 2天前
- 29热度
- 0评论
适用版本: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;反向还原需手动处理或使用第三方库(如 dacite、cattrs)。
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 补全 | 好 | 好 | 差 | 好 |
| 学习成本 | 低 | 低 | 最低 | 中 |
选择建议:
- 内部配置 / 简单 DTO →
dataclass - 不可变、轻量、可当 key →
NamedTuple或frozen 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
最佳实践清单
- 可变默认值一律用
field(default_factory=...) - 配置类优先 dataclass,需要校验再上 Pydantic
- 不想被意外修改的配置用
frozen=True - 序列化用
asdict(),反序列化过滤未知键 - 复杂初始化逻辑放
__post_init__,保持字段声明简洁 - 大型项目里按职责拆分多个小 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 是第三方先驱,功能更丰富;标准库 dataclasses 受 attrs 启发,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")
延伸阅读
- 官方文档:dataclasses — Data Classes
- PEP 557 — Data Classes
- PEP 681 — Data Class Transforms(3.11+ 类型检查增强)
文档版本:2026-06 | 示例代码均在 Python 3.10+ 验证通过