Python 数据类 dataclasses 深度实战——与 Pydantic、attrs 的对比选型
2026/4/30大约 6 分钟
Python 数据类 dataclasses 深度实战——与 Pydantic、attrs 的对比选型
适读人群:Python 工程师,想搞清楚数据类选型的 | 阅读时长:约 15 分钟 | 核心价值:搞懂 dataclasses、Pydantic、attrs 的核心区别,做出适合项目的选型决策
三个月用过三个库之后
刚开始做 AI 项目后端,我花了三个月先后用了三个数据类库——第一个月用原生 Python 类 + @property,第二个月发现别人都在用 dataclasses 就改了,第三个月进了一个用 Pydantic 的项目,又改了。
这三次切换,每次都伴随着大量的代码迁移和踩坑。
如果当时有人告诉我这三个库分别适合什么场景,我能省下很多时间。这篇文章,就是这样一个简明的选型指南。
一、dataclasses:标准库的最优解
1.1 基础用法
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class User:
id: int
name: str
email: str
age: Optional[int] = None
tags: list[str] = field(default_factory=list)
score: float = 0.0
# 自动生成 __init__、__repr__、__eq__
user = User(id=1, name="老张", email="laoz@example.com")
print(user)
# User(id=1, name='老张', email='laoz@example.com', age=None, tags=[], score=0.0)
user2 = User(id=1, name="老张", email="laoz@example.com")
print(user == user2) # True(自动生成 __eq__)1.2 常用参数
from dataclasses import dataclass, field
import json
@dataclass(
order=True, # 生成 __lt__、__le__ 等比较方法
frozen=True, # 不可变,生成 __hash__(可用作字典 key)
slots=True, # Python 3.10+,用 __slots__ 减少内存
)
class Point:
x: float
y: float
p1 = Point(1.0, 2.0)
p2 = Point(3.0, 4.0)
print(p1 < p2) # True(按 x 排序)
print(hash(p1)) # 可哈希
# 有序数据类
points = [Point(3, 1), Point(1, 5), Point(2, 3)]
print(sorted(points)) # [Point(x=1.0, y=5.0), Point(x=2.0, y=3.0), Point(x=3.0, y=1.0)]1.3 post_init:初始化后处理
from dataclasses import dataclass, field
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError(f"半径必须大于 0,得到: {self.radius}")
@property
def area(self) -> float:
import math
return math.pi * self.radius ** 21.4 继承
@dataclass
class Animal:
name: str
sound: str
@dataclass
class Dog(Animal):
breed: str
sound: str = "汪汪" # 子类可以覆盖默认值
def speak(self) -> str:
return f"{self.name}({self.breed})说:{self.sound}"
d = Dog(name="旺财", breed="柴犬")
print(d.speak()) # 旺财(柴犬)说:汪汪二、三库核心对比
2.1 功能矩阵
| 功能 | dataclasses | Pydantic v2 | attrs |
|---|---|---|---|
| 运行时类型验证 | 无 | 完整 | 可选(validators) |
| 性能 | 快 | v2 用 Rust,很快 | 最快 |
| JSON 序列化 | 需要手写 | 内置 | 需要 cattrs |
| 嵌套模型验证 | 无 | 递归验证 | 可选 |
| FastAPI 集成 | 不直接支持 | 原生集成 | 不支持 |
| 类型注解依赖 | 必须 | 必须 | 可选 |
| 标准库 | 是 | 否 | 否 |
| 不可变支持 | frozen=True | 可配置 | on_setattr |
| 学习成本 | 低 | 中 | 中 |
2.2 选型决策树
你的场景是什么?
│
├─ FastAPI / 对外 API 接口 / 需要数据验证?
│ → Pydantic v2
│
├─ 纯粹的数据容器,不需要验证,只在项目内部用?
│ → dataclasses(标准库,无额外依赖)
│
├─ 性能极端敏感、大量数据类实例、需要高度定制化?
│ → attrs
│
└─ 不确定?
→ dataclasses 起步,有验证需求时迁移到 Pydantic三、完整可运行示例:三库对比演示
#!/usr/bin/env python3
"""
dataclasses vs Pydantic v2 vs attrs 完整对比演示
"""
from __future__ import annotations
import time
from dataclasses import dataclass, field
from typing import Optional
# ===== 1. dataclasses 版本 =====
@dataclass
class UserDC:
id: int
name: str
email: str
age: Optional[int] = None
tags: list[str] = field(default_factory=list)
score: float = 0.0
def __post_init__(self):
# 手动验证
if self.score < 0:
raise ValueError("score 不能为负")
if self.age is not None and self.age < 0:
raise ValueError("age 不能为负")
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"email": self.email,
"age": self.age,
"tags": self.tags,
"score": self.score,
}
# ===== 2. Pydantic v2 版本 =====
try:
from pydantic import BaseModel, Field, field_validator
class UserPydantic(BaseModel):
id: int
name: str
email: str
age: Optional[int] = None
tags: list[str] = Field(default_factory=list)
score: float = Field(default=0.0, ge=0)
@field_validator("age")
@classmethod
def validate_age(cls, v):
if v is not None and v < 0:
raise ValueError("age 不能为负")
return v
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v:
raise ValueError("邮箱格式无效")
return v.lower()
PYDANTIC_AVAILABLE = True
except ImportError:
PYDANTIC_AVAILABLE = False
print("Pydantic 未安装,跳过 Pydantic 演示")
# ===== 3. attrs 版本 =====
try:
import attr
@attr.s(auto_attribs=True, slots=True)
class UserAttrs:
id: int
name: str
email: str
age: Optional[int] = None
tags: list[str] = attr.Factory(list)
score: float = attr.ib(default=0.0, validator=attr.validators.ge(0))
ATTRS_AVAILABLE = True
except ImportError:
ATTRS_AVAILABLE = False
print("attrs 未安装,跳过 attrs 演示")
# ===== 性能基准测试 =====
def benchmark_creation(cls, kwargs: dict, n: int = 100_000) -> float:
start = time.perf_counter()
for _ in range(n):
cls(**kwargs)
return (time.perf_counter() - start) * 1000
# ===== 特性演示 =====
def demo_dataclasses():
print("=== dataclasses 演示 ===\n")
u1 = UserDC(id=1, name="老张", email="laoz@example.com", score=95.0, tags=["python"])
u2 = UserDC(id=1, name="老张", email="laoz@example.com", score=95.0, tags=["python"])
print(f"实例: {u1}")
print(f"相等性: u1 == u2 → {u1 == u2}")
print(f"字典: {u1.to_dict()}")
# 验证
try:
UserDC(id=2, name="测试", email="test@test.com", score=-1)
except ValueError as e:
print(f"验证错误: {e}")
# 不可变
from dataclasses import dataclass as dc
@dc(frozen=True)
class ImmutablePoint:
x: float
y: float
p = ImmutablePoint(1.0, 2.0)
try:
p.x = 3.0
except Exception as e:
print(f"不可变错误: {type(e).__name__}: {e}")
print()
def demo_pydantic():
if not PYDANTIC_AVAILABLE:
return
print("=== Pydantic v2 演示 ===\n")
# 类型强制转换
u = UserPydantic(id="1", name="老张", email="LaOZ@Example.COM", score="95.5")
print(f"实例: {u}")
print(f"id 类型: {type(u.id).__name__} (从字符串 '1' 转换)")
print(f"email: {u.email} (自动小写)")
# 验证错误
try:
UserPydantic(id=2, name="错误用户", email="invalid-email", score=-1)
except Exception as e:
print(f"验证错误数量: {len(e.errors())}")
for err in e.errors():
print(f" - {err['loc']}: {err['msg']}")
# JSON 序列化
json_str = u.model_dump_json(indent=2)
print(f"\nJSON:\n{json_str}")
# 从字典构建
data = {"id": 3, "name": "小陈", "email": "xc@example.com"}
u2 = UserPydantic.model_validate(data)
print(f"\n从字典构建: {u2}")
print()
def demo_performance():
print("=== 性能对比(创建 100000 个实例)===\n")
kwargs = {
"id": 1,
"name": "老张",
"email": "laoz@example.com",
"score": 95.0,
}
t_dc = benchmark_creation(UserDC, kwargs)
print(f"dataclasses: {t_dc:.1f}ms")
if PYDANTIC_AVAILABLE:
t_pydantic = benchmark_creation(UserPydantic, kwargs)
print(f"Pydantic v2: {t_pydantic:.1f}ms")
if ATTRS_AVAILABLE:
t_attrs = benchmark_creation(UserAttrs, kwargs)
print(f"attrs: {t_attrs:.1f}ms")
def main():
demo_dataclasses()
demo_pydantic()
demo_performance()
if __name__ == "__main__":
main()四、踩坑实录 1:可变默认值
from dataclasses import dataclass
# 错误:直接用列表作为默认值
@dataclass
class BadUser:
tags: list = [] # dataclasses 会拒绝这个!
# TypeError: mutable default <class 'list'> is not allowed
# 正确:用 field(default_factory=...)
from dataclasses import field
@dataclass
class GoodUser:
tags: list[str] = field(default_factory=list)
metadata: dict = field(default_factory=dict)五、踩坑实录 2:dataclasses 继承中有默认值的字段顺序
# 错误:父类有默认值,子类有无默认值的字段,顺序冲突
@dataclass
class Parent:
name: str
score: float = 0.0 # 有默认值
@dataclass
class Child(Parent):
id: int # 无默认值,但在 score 后面 → TypeError!
# TypeError: non-default argument 'id' follows default argument
# 正确:无默认值的字段必须在所有有默认值的字段之前
@dataclass
class Parent2:
name: str
@dataclass
class Child2(Parent2):
id: int # 无默认值
score: float = 0.0 # 有默认值放最后六、踩坑实录 3:Pydantic model_copy 和 Python copy 的区别
from pydantic import BaseModel
import copy
class Config(BaseModel):
host: str = "localhost"
port: int = 5432
tags: list[str] = []
cfg = Config(tags=["db", "primary"])
# dataclasses.replace / Pydantic model_copy:创建新对象并修改部分字段
cfg2 = cfg.model_copy(update={"port": 5433}) # 推荐方式
print(cfg2) # host='localhost' port=5433 tags=['db', 'primary']
print(cfg) # 原始对象不变
# Python copy.copy 是浅拷贝
cfg3 = copy.copy(cfg)
cfg3.tags.append("replica") # 修改 tags
print(cfg.tags) # ['db', 'primary', 'replica'] —— 原始对象的 tags 也被改了!
# 正确:深拷贝
cfg4 = copy.deepcopy(cfg)总结
三库选型口诀:
- dataclasses:内部数据容器,零依赖,简单够用就选它
- Pydantic:对外 API,需要验证和序列化,和 FastAPI 配合用
- attrs:极致性能,高度定制,成熟项目或框架开发用
99% 的日常项目,从 dataclasses 起步,有验证需求时迁移到 Pydantic,这是最稳的路径。
