为什么需要数据类

业务代码里到处是「装着字段的对象」:订单、用户、配置项、接口返回的模型。用普通类写,每个都要手写 `__init__`、`__repr__`、`__eq__`,字段一多代码就膨胀,且容易在复制粘贴时漏掉某个字段。用字典替代又会失去属性访问的便利和类型约束。

Python 3.7 引入的 `dataclasses` 模块就是为解决这个问题:只要声明类变量并标注类型,`@dataclass` 装饰器会自动生成 `__init__`、`__repr__`、`__eq__` 等方法,让「数据容器」类的编写成本降到最低。

from dataclasses import dataclass

@dataclass
class Product:
    sku: str
    name: str
    price: float
    stock: int = 0

这段代码生成的类自带初始化、打印和相等比较,三个字段加一个默认值,语义一目了然。数据类把「数据长什么样」和「行为是什么」分开:纯数据用数据类,复杂行为再叠方法或组合。

数据类解决的不是性能问题,而是代码表达问题:让数据模型本身清晰可见。
本文核心知识点
  • @dataclass自动生成样板方法。
  • field精细字段配置。
  • frozen不可变数据类。
  • property属性校验与派生。
Python 数据类配图:蓝调插画中层叠半透明数据卡片与菱形节点,发光连线串成网格,体现字段与数据模型管理
数据模型像一组有序卡片,字段清晰、关系明确,代码才好维护。

@dataclass 基础用法

`@dataclass` 默认按「无默认值的字段在前、有默认值的在后」的规则生成构造方法,这与 Python 函数参数的规则一致。实例化时用位置参数或关键字参数都可以,打印时得到可读的 `Product(sku='A1', name='键盘', price=299.0, stock=20)`。

p1 = Product("A1", "键盘", 299.0, 20)
p2 = Product(sku="A1", name="键盘", price=299.0, stock=20)
print(p1)
print(p1 == p2)

数据类默认参与相等比较的是全部字段;想只按某个业务主键比较,可以在 `@dataclass(eq=True)` 基础上重写 `__eq__`,或者用 `field(compare=False)` 排除不参与比较的字段。字段顺序会直接影响 `__init__` 的参数顺序,定义时把稳定、必填的字段放前面。

字段配置与默认值

默认值不是只能写字面量。列表、字典这类可变对象直接作为默认值是 Python 的经典陷阱:所有实例共享同一个列表,改一个全变。数据类用 `field(default_factory=list)` 这类工厂写法让每个实例各自创建新容器。

from dataclasses import dataclass, field

@dataclass
class Cart:
    owner: str
    items: list = field(default_factory=list)
    tags: dict = field(default_factory=dict)

c1 = Cart("小明")
c1.items.append("键盘")
c2 = Cart("小红")
print(c2.items)  # 输出空列表,两个实例互不影响

`field` 还能控制字段的附加行为:`init=False` 表示不进构造方法,常配合 `default_factory` 存派生数据;`repr=False` 让敏感或冗余字段不进打印输出;`compare=False` 排除在相等比较之外;`metadata` 可以挂业务元数据,供工具读取。

frozen 与不可变数据类

把 `frozen=True` 传给装饰器,数据类实例就变得不可变:任何赋值都会抛 `FrozenInstanceError`。不可变对象天然线程安全、可安全作字典键、方便做值语义比较,在多线程和函数式风格里非常受用。

@dataclass(frozen=True)
class Point:
    x: float
    y: float

pt = Point(1.0, 2.0)
pt.x = 3.0  # 抛 FrozenInstanceError

注意 `frozen` 只拦截属性赋值,不拦截容器内部修改:`Point` 里如果放了一个列表字段,列表里的元素仍可增删。要彻底不可变,容器字段也要用元组这类不可变类型。Python 3.10 之后还能在 `@dataclass(slots=True)` 上开槽位存储,省内存、提访问速度,代价是类不能再有额外动态属性。

property 与属性校验

数据类适合装纯数据,但真实业务往往要在赋值时做校验或派生:价格不能为负、总量要缓存计算、格式化字段要按规则输出。`property` 把「看似普通的属性访问」接到方法上,读时计算、写时校验,对外接口不变。

@dataclass
class Order:
    unit_price: float
    quantity: int

    @property
    def total(self):
        return self.unit_price * self.quantity

    @total.setter
    def total(self, value):
        if value < 0:
            raise ValueError("金额不能为负")
        self.unit_price = value / self.quantity

更强的做法是在 `__post_init__` 里做跨字段校验:这个钩子在 `__init__` 末尾自动执行,适合检查「结束时间晚于开始时间」「折扣不超过原价」这类多个字段的组合约束,校验失败直接抛异常,让非法对象在诞生那一刻就被拦住。