@dataclass 是 python 3.7+ 新项目的默认选择,因其零依赖、原生类型提示及 ide/mypy 开箱即用;仅当需 attrs 的运行时验证、json 钩子或旧版兼容时才选用 attrs。

dataclass 是默认选择,除非你明确需要 attrs 的某项能力
Python 3.7+ 项目里,@dataclass 已是事实标准:零依赖、类型提示原生支持、IDE 和静态检查工具(如 mypy)开箱即用。NASA 用 attrs 做火星任务,不等于你写个配置类或 API 响应模型也得上 attrs——那是为极端可定制性与历史兼容性付出的额外维护成本。
- 新项目、内部工具、fastapi/Pydantic 模型层 → 优先用
@dataclass - 需要运行时字段验证、JSON 序列化钩子、或兼容 Python pydantic.BaseModel
- 团队已有 attrs 代码库且字段逻辑复杂(比如大量
attr.ib(converter=...)或自定义__attrs_post_init__)→ 不必强行迁移
字段默认值写法不同,field(default_factory=list) 容易漏掉 default_factory
这是 dataclass 和 attrs 最常踩的坑:可变对象(list, dict)作默认值时,Python 会共享同一个对象实例。两者都要求显式用工厂函数,但语法位置和命名不一致。
- dataclass:
tags: list = field(default_factory=list)—— 必须用field()包裹,default_factory是关键字参数 - attrs:
tags: list = attr.ib(factory=list)——factory是attr.ib()的参数,不是default_factory - 错写成
tags: list = []或tags: list = attr.ib(default=[])→ 多个实例共用同一列表,修改一个,全部“同步”变
序列化和校验不是 dataclass 的事,别硬塞 __post_init__
@dataclass 只负责结构定义和基础协议(__init__, __repr__, __eq__),它不校验字段类型、不处理缺失字段、也不自动转 JSON。有人在 __post_init__ 里手动 raise TypeError 或调 json.dumps(self.__dict__),这等于把 pydantic 的活儿自己干,还干不全。
- 需要字段级校验(比如
age > 0)、缺失字段报错、或从 dict/json 自动加载 → 直接换pydantic.BaseModel - 只需要轻量序列化(如日志打点、简单导出)→ 用
dataclasses.asdict()或dataclasses.astuple(),别自己遍历__dict__ - 想加字段文档或元数据?
Field(metadata={...})支持,但 IDE 不识别,不如注释或用 pydantic 的Field(description=...)
attrs 的 @define 在 2025 年仍比 dataclass 多两个实用特性
attrs 并没过时,它在两个具体场景仍有不可替代性:一是运行时字段访问控制(kw_only=True 强制关键字传参),二是细粒度冻结行为(frozen=True 下仍允许特定字段在 __attrs_post_init__ 中修改)。dataclass 的 frozen 是全有或全无。
立即学习“Python免费学习笔记(深入)”;
-
@define(kw_only=True):调用User("alice", 25)会报错,必须写User(name="alice", age=25)—— 对 API 参数类防误用很有效 -
@define(frozen=True, slots=True)+self._cache = None在__attrs_post_init__中赋值 → 冻结对象的同时保留缓存字段,dataclass 做不到这点 - 但注意:attrs 需要
pip install attrs,而 dataclass 是标准库 —— CI 环境少一个依赖,就是少一个故障点
真正该纠结的不是 dataclass 还是 attrs,而是「这个类到底需不需要运行时行为」。纯数据容器就 dataclass;带校验/序列化/外部交互就 pydantic;只有当你需要 attrs 特有的字段级控制权,且不愿引入 pydantic 时,才值得多装一个包。