dataclass 与数据对象
约 2207 字大约 7 分钟
2026-05-10
在 Python 中,我们经常需要定义一些“主要用来保存数据”的类。例如用户、商品、订单、坐标点、配置项等。
- 基本用法
- 默认值
- 可变默认值的陷阱
- postinit
- 1把一个手写 __init__/__repr__/__eq__ 的“数据袋”类换成 @dataclass,对比代码量。
- 2故意写 items: list = [] —— 看 ValueError 拒绝可变默认;改成 field(default_factory=list) 正确。
- 3用 __post_init__ 写一段“price < 0 时抛错”的校验,体会“dataclass 解决样板,校验仍需自己写”。
- 4把不变的 Point/Money 写成 @dataclass(frozen=True),赋值时看 FrozenInstanceError。
- 5对比 @dataclass(order=True) + sorted(list_of_obj) 和手写 __lt__ 的差异 —— 数据为主时 dataclass 几乎总是更划算。
在 Python 中,我们经常需要定义一些“主要用来保存数据”的类。例如用户、商品、订单、坐标点、配置项等。
如果使用普通类来写,代码会比较重复:
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
def __repr__(self):
return f'User(name={self.name!r}, age={self.age!r}, email={self.email!r})'
def __eq__(self, other):
if not isinstance(other, User):
return False
return self.name == other.name and self.age == other.age and self.email == other.email这个类真正重要的只有三个字段:name、age、email。但为了初始化、打印、比较,我们写了很多样板代码。
dataclass 就是为了解决这个问题的。
基本用法
dataclass 来自标准库 dataclasses。它可以根据类型注解自动生成 __init__、__repr__、__eq__ 等方法。
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str
user = User('Alice', 18, '[email protected]')
print(user)输出:
User(name='Alice', age=18, email='[email protected]')上面的 User 等价于帮我们自动写好了类似这样的代码:
class User:
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email但 dataclass 还额外生成了更友好的 __repr__ 和 __eq__。
默认值
字段可以设置默认值:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int = 18
active: bool = True
user = User('Alice')
print(user)输出:
User(name='Alice', age=18, active=True)注意:没有默认值的字段必须写在有默认值的字段前面。
下面这样会报错:
@dataclass
class User:
age: int = 18
name: str因为生成的 __init__ 会变成类似 __init__(age=18, name),这不符合 Python 函数参数规则。
可变默认值的陷阱
普通类中常见的一个坑是可变默认值:
class Team:
def __init__(self, members=[]):
self.members = members多个对象会共享同一个列表,导致数据互相污染。
dataclass 会直接阻止这种写法:
from dataclasses import dataclass
@dataclass
class Team:
members: list[str] = [] # ValueError正确做法是使用 field(default_factory=...):
from dataclasses import dataclass, field
@dataclass
class Team:
name: str
members: list[str] = field(default_factory=list)
team1 = Team('A')
team2 = Team('B')
team1.members.append('Alice')
print(team1.members) # ['Alice']
print(team2.members) # []default_factory=list 的含义是:每次创建对象时,都调用一次 list(),为当前对象创建一个独立的新列表。
__post_init__
如果对象创建后还需要额外处理,可以定义 __post_init__() 方法。它会在自动生成的 __init__() 执行完后被调用。
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int
def __post_init__(self):
if self.price < 0:
raise ValueError('价格不能为负数')
if self.quantity < 0:
raise ValueError('数量不能为负数')
product = Product('Book', 59.9, 10)__post_init__() 常用于:
- 参数校验
- 字段标准化
- 计算派生字段
- 初始化不能直接从参数得到的属性
例如:
from dataclasses import dataclass, field
@dataclass
class OrderItem:
price: float
quantity: int
total: float = field(init=False)
def __post_init__(self):
self.total = self.price * self.quantity
item = OrderItem(20, 3)
print(item.total) # 60init=False 表示这个字段不作为 __init__ 的参数,而是在对象内部计算出来。
冻结对象:frozen=True
如果希望对象创建后不能再修改,可以使用 frozen=True。
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
point = Point(1, 2)
point.x = 100 # FrozenInstanceError冻结对象适合表达不可变值对象,例如坐标点、金额、颜色、配置快照等。
@dataclass(frozen=True)
class Money:
amount: int
currency: str不可变对象更安全,也更容易推理,因为它的状态不会在程序运行过程中被悄悄改掉。
排序:order=True
默认情况下,dataclass 只生成 __eq__,不生成大小比较方法。如果希望对象可以排序,可以使用 order=True。
from dataclasses import dataclass
@dataclass(order=True)
class Student:
score: int
name: str
students = [
Student(90, 'Alice'),
Student(85, 'Bob'),
Student(95, 'Carol'),
]
print(sorted(students))排序时会按照字段定义顺序依次比较。上例会先比较 score,如果分数相同,再比较 name。
如果不希望某个字段参与比较,可以使用 field(compare=False)。
from dataclasses import dataclass, field
@dataclass(order=True)
class Student:
score: int
name: str = field(compare=False)控制字段行为
field() 可以更细致地控制字段。
from dataclasses import dataclass, field
@dataclass
class User:
name: str
password: str = field(repr=False)repr=False 表示打印对象时不显示这个字段:
user = User('Alice', 'secret')
print(user) # User(name='Alice')常用参数:
| 参数 | 作用 |
|---|---|
default | 设置默认值 |
default_factory | 设置默认值工厂,常用于 list、dict、set |
init | 是否出现在 __init__ 参数中 |
repr | 是否出现在 __repr__ 中 |
compare | 是否参与比较 |
转换为字典或元组
标准库提供了 asdict() 和 astuple()。
from dataclasses import dataclass, asdict, astuple
@dataclass
class User:
name: str
age: int
user = User('Alice', 18)
print(asdict(user)) # {'name': 'Alice', 'age': 18}
print(astuple(user)) # ('Alice', 18)这在序列化、测试断言、接口返回数据时很方便。
dataclass 与普通 class 的区别
dataclass 不是一种新的类类型。它本质上还是普通的 Python 类,只是帮我们自动生成了一些常用方法。
适合使用 dataclass 的场景:
- 主要职责是保存数据
- 字段比较固定
- 需要清晰的类型注解
- 希望减少
__init__、__repr__等样板代码 - 值对象、配置对象、DTO、简单领域对象
不太适合使用 dataclass 的场景:
- 类的行为远比数据重要
- 对象内部状态变化复杂
- 初始化流程非常复杂
- 需要严格控制属性访问和不变量
- 只是为了少写几行代码而强行套用
dataclass 与类型提示
dataclass 强依赖类型注解来识别字段。
@dataclass
class User:
name: str
age: int这里的 name 和 age 会被识别为字段。
如果没有类型注解,dataclass 不会把它当作普通字段:
@dataclass
class User:
name = 'anonymous' # 不会成为 __init__ 参数需要注意:Python 默认不会在运行时强制检查类型。
user = User(name=123, age='eighteen') # 默认不会报类型错误类型注解主要服务于:
- IDE 自动补全
- 静态检查工具
- 代码可读性
- 文档表达
如果需要运行时校验,应该在 __post_init__() 中手动检查,或者使用 Pydantic 这类库。
业务示例:订单明细
from dataclasses import dataclass, field, asdict
from datetime import datetime
@dataclass(frozen=True)
class OrderItem:
sku: str
name: str
price: float
quantity: int
total: float = field(init=False)
def __post_init__(self):
if self.price < 0:
raise ValueError('价格不能为负数')
if self.quantity <= 0:
raise ValueError('数量必须大于 0')
object.__setattr__(self, 'total', self.price * self.quantity)
@dataclass
class Order:
order_no: str
items: list[OrderItem] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.now)
def add_item(self, item: OrderItem):
self.items.append(item)
@property
def amount(self):
return sum(item.total for item in self.items)
order = Order('NO001')
order.add_item(OrderItem('B001', 'Python Book', 59.9, 2))
order.add_item(OrderItem('P001', 'Pen', 3.5, 5))
print(order.amount)
print(asdict(order))这里:
OrderItem是不可变的值对象Order是可变的业务对象items使用default_factory=list避免共享列表total由__post_init__()计算amount使用@property动态计算
注意事项
dataclass自动生成代码,但不会自动帮你设计好对象职责。- 可变默认值要使用
field(default_factory=...)。 frozen=True下如果要在__post_init__()中设置字段,需要使用object.__setattr__()。- 类型注解默认不做运行时检查。
- 不要把所有类都写成 dataclass。数据为主时才适合。
总结
dataclass 适合用来定义数据对象,它能减少大量样板代码,让类的字段结构更清晰。
@dataclass自动生成初始化、打印、比较等方法field(default_factory=...)用来处理可变默认值__post_init__()用来做初始化后的校验和补充处理frozen=True可以创建不可变对象order=True可以让对象支持排序
它不是面向对象设计的替代品,而是 Python 提供的一个实用工具:当一个类主要用来表达数据结构时,dataclass 会让代码更简洁、更可靠。
- @dataclass 根据类型注解自动生成 __init__/__repr__/__eq__,消除“数据袋”类的样板代码。
- 可变默认必须用 field(default_factory=list/dict/set/...),否则 dataclass 直接拒绝你写错。
- __post_init__ 做初始化后处理:校验、派生字段(配合 field(init=False))、参数标准化。
- frozen=True 得不可变值对象;order=True 让对象按字段顺序可排序。
- dataclass 不强制类型;运行时要校验类型可在 __post_init__ 写,或者用 Pydantic 这种重型工具。
版权所有
版权归属:Shuo Liu
