第 29 课 · 进阶 · 19 分钟

Python dataclasses

用声明式字段定义以数据为中心的类,减少重复样板。

dataclasses字段默认值asdict

订单是一条小记录:编号、明细、总价、是否付款。手写 __init__、__repr__、__eq__ 就是整整一页样板。@dataclass 免费生成了什么?

dataclass 是带有自动生成方法的普通类,初始化、显示和相等比较都不必手写。它适合点、用户、配置值这类小型记录。字段通过注解声明,默认值则像普通赋值一样写在后面。 你还会用 default_factory、在 __post_init__ 中校验,并用 replace 更新冻结记录。

用注解声明字段

@dataclass 装饰器读取带注解的类属性,并自动生成 __init__。创建 Point(2, 3) 会按顺序给 x 和 y 赋值。自动生成的 __repr__ 让对象在控制台里更易读。

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

point = Point(2, 3)
print(point)

一个小型记录

添加默认值

带默认值的字段可以在构造时省略。没有默认值的字段必须放在类体前面。repr 会同时显示字段名和当前值。

from dataclasses import dataclass

@dataclass
class User:
    name: str
    active: bool = True

print(User("Ada"))

字段列表中的默认值

比较与转换

dataclass 会根据字段生成 __eq__,所以数据相同的两个对象相等。asdict 返回普通字典,便于 JSON 或测试断言。frozen=True 会禁止之后再给属性赋值。

from dataclasses import dataclass, asdict

@dataclass(frozen=True)
class Item:
    name: str
    price: float

item = Item("tea", 3.5)
print(asdict(item))
print(Item("tea", 3.5) == item)

相等比较与普通映射

可变默认值需要工厂

dataclass 里的列表默认值会被所有实例共享,所以 Python 会直接拒绝。field(default_factory=list) 会为每个对象创建新列表。字典、集合和其他可变值同理。

from dataclasses import dataclass, field

@dataclass
class Order:
    drink: str
    tags: list[str] = field(default_factory=list)

first = Order("latte")
second = Order("tea")
first.tags.append("gift")
print(first.tags)
print(second.tags)
print(first.tags is second.tags)

每个实例都有自己的新列表

__post_init__ 校验并派生

__post_init__ 在自动生成的 __init__ 之后运行。可以用它拒绝不可能的值,并计算必须保持一致的字段。它应该校验业务规则,而不是试图强制执行 Python 根本没检查过的类型提示。

from dataclasses import dataclass

@dataclass
class Order:
    price: float
    quantity: int
    total: float = 0.0

    def __post_init__(self):
        if self.price < 0 or self.quantity <= 0:
            raise ValueError("invalid order")
        self.total = round(self.price * self.quantity, 2)

for values in [(4.5, 2), (-1, 1)]:
    try:
        print(Order(*values))
    except ValueError as error:
        print(type(error).__name__, error)

先校验,再派生总价

用 replace 更新冻结记录

frozen=True 禁止属性赋值,让记录可以安全共享。replace() 返回一个新实例,只改动指定字段,原对象保持不变。这个模式适合撤销、历史记录和配置快照。

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Order:
    drink: str
    price: float

original = Order("latte", 4.5)
updated = replace(original, price=5.0)
print(original)
print(updated)
print(original == updated)

复制后修改,而不是原地改

本课要点

  • @dataclass 会生成 __init__、__repr__ 和 __eq__。
  • 字段是带注解的类属性。
  • 带默认值的字段必须放在没有默认值的字段之后。
  • asdict 会把 dataclass 实例转换成字典。
  • frozen=True 会让实例不可变。
  • 可变字段用 default_factory;__post_init__ 负责校验;冻结记录用 replace 更新。

常见错误

  • 把没有默认值的字段放在有默认值的字段之后。
  • 以为 dataclass 字段会在运行时校验类型。
  • 试图给 frozen 实例赋值。
  • 写 tags: list[str] = [],让所有实例共享同一个列表。
轮到你

创建一个带 name 和 price 的 frozen Item dataclass,再打印 asdict(Item('tea', 3.5))。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← 上下文管理器itertools →