数据类 (Data Classes)
导语
在 Python 中,我们常常需要创建仅用于存储数据的类——比如表示坐标的点、配置参数、API 响应模型等。一个典型的"数据容器"类往往充斥着重复的样板代码:__init__ 赋值、__repr__ 调试输出、__eq__ 相等比较……这些工作乏味且容易出错。Python 3.7 引入的 dataclasses 模块通过 @dataclass 装饰器自动生成这些样板方法,让你用最少代码声明数据结构。数据类在概念上类似命名元组(namedtuple),但更灵活——支持类型注解、默认值、继承和自定义初始化逻辑,是现代 Python 项目中构建数据载体的首选方式。
学习目标
- 掌握
@dataclass装饰器的基本用法,理解自动生成的__init__、__repr__、__eq__方法 - 学会使用
field()配置默认值、default_factory和frozen=True不可变语义 - 理解
__post_init__钩子和数据类继承的用法与限制
概念介绍
dataclasses 模块的核心是 @dataclass 装饰器。它扫描类体中带类型注解的字段,自动生成一系列特殊方法:
__init__— 根据字段声明生成构造函数,支持默认值__repr__— 生成可读的字符串表示,格式为ClassName(field1=value1, field2=value2)__eq__— 按字段值逐个比较,判断两个实例是否相等__lt__、__le__、__gt__、__ge__— 比较运算(需设置order=True)__hash__— 哈希值(仅当frozen=True或unsafe_hash=True时生成)
field() 函数提供字段的细粒度控制,常用参数包括:default(默认值)、default_factory(无参工厂函数,生成可变默认值)、init=False(不参与构造函数)、repr=False(不在 __repr__ 中展示)、frozen=True(该字段不可变)、compare=False(不参与 __eq__ 比较)等。
tip
数据类与命名元组(namedtuple)的关系:namedtuple 是不可变的 tuple 子类,侧重节省内存和向后兼容;dataclass 是普通 class,支持默认值、修改字段、继承和 __post_init__,适用范围更广。两者都可用 _asdict() / dataclasses.asdict() 转为字典。
代码示例
示例 1:基本 @dataclass — 自动生成常用方法
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Person:
name: str
age: int
email: str = ""
# Auto-generated __init__: constructor with typed parameters
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
p3 = Point(1.0, 2.0)
# Auto-generated __repr__: clean debug-friendly string representation
print(p1) # Point(x=3.0, y=4.0)
# Auto-generated __eq__: field-by-field value comparison
print(p1 == p2) # True
print(p1 == p3) # False
# Person with optional default value
alice = Person("Alice", 30)
bob = Person("Bob", 25, "bob@example.com")
print(alice) # Person(name='Alice', age=30, email='')
print(bob) # Person(name='Bob', age=25, email='bob@example.com')
# Mutability: fields can be reassigned by default
p1.x = 10.0
print(p1) # Point(x=10.0, y=4.0)
@dataclass 只处理带类型注解的类变量。没有类型注解的赋值不被视为数据字段。注意:先声明无默认值字段、再声明有默认值字段会引发 SyntaxError——因为 Python 无法判断构造函数中哪些参数是必传的。如果有默认值的字段放在前面,没有默认值的字段放在后面,调用时会导致参数位置混乱,这与 Python 函数参数的规则一致。
warning
字段声明顺序:没有默认值的字段必须排在有默认值的字段之前。
# BAD — raises dataclasses.SlotWrapperError / SyntaxError at call site
@dataclass
class Bad:
name: str = "Alice"
age: int # ERROR: non-default argument follows default argument
示例 2:field() 配置 — default_factory 与 frozen=True
from dataclasses import dataclass, field
from typing import List
@dataclass
class Classroom:
teacher: str
students: List[str] = field(default_factory=list)
max_size: int = field(default=30, repr=False) # hidden from __repr__
# default_factory avoids shared mutable state between instances
room_a = Classroom("Ms. Wang")
room_a.students.append("Alice")
room_b = Classroom("Mr. Li")
# room_b.students is []. Without default_factory, it would incorrectly share the same list
print(room_a.students) # ['Alice']
print(room_b.students) # []
print(room_a) # Classroom(teacher='Ms. Wang', students=['Alice']) — max_size hidden
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool = False
cfg = Config("localhost", 8080)
print(cfg) # Config(host='localhost', port=8080, debug=False)
# Reassignment raises dataclasses.FrozenInstanceError — instance is now immutable
try:
cfg.port = 9090
except dataclasses.FrozenInstanceError as e:
print(f"FrozenInstanceError: {e}")
为什么需要 default_factory? 如果直接使用 students: List[str] = [],所有实例会共享同一个列表对象——这是 Python 中经典的"可变默认值坑"。default_factory 接受一个无参可调对象,每次实例化时调用它来生成独立的新对象。list、dict、set 以及任何无参工厂函数都可以作为 default_factory 的值。
tip
frozen=True 使实例变为不可变的——所有字段赋值都会触发 FrozenInstanceError。这让你可以用数据类安全地表示配置值、常量集合等不应被修改的数据。
示例 3:数据类继承与 __post_init__ 钩子
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Shape:
name: str
color: str = "white"
_area: Optional[float] = field(default=None, init=False)
def area(self) -> Optional[float]:
return self._area
@dataclass
class Rectangle(Shape):
width: float
height: float
def __post_init__(self):
# Validation and computed field assignment after auto-generated __init__
if self.width <= 0 or self.height <= 0:
raise ValueError("Width and height must be positive")
self._area = self.width * self.height
@dataclass
class Circle(Shape):
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError("Radius must be positive")
self._area = 3.14159 * self.radius ** 2
rect = Rectangle("Photo Frame", "blue", 10.0, 5.0)
print(rect) # Rectangle(name='Photo Frame', color='blue', width=10.0, height=5.0)
print(rect.area()) # 50.0
circle = Circle("Coin", "gold", 3.0)
print(circle) # Circle(name='Coin', color='gold', radius=3.0)
print(circle.area()) # 28.274309999999997
# Inheritance: parent fields come first, then child fields
# Shape(name, color, _area) → Rectangle(name, color, width, height)
# _area has init=False, so constructor is (name, color, width, height)
__post_init__ 在自动生成的 __init__ 的末尾被调用,适合做两件事:字段校验(确保数据合法性)和计算字段(根据其他字段推导值)。带 init=False 的字段不会出现在构造函数中,只能通过 __post_init__ 或实例方法赋值。继承时,父类字段排在构造函数参数的前面,子类字段排在后面。
note
dataclasses 在 Python 3.10+ 支持 kw_only=True,允许将指定字段标记为仅关键字参数,进一步降低继承时参数顺序的问题。
常见错误与解决
warning
错误 1:可变默认值共享(mutable default argument)
from dataclasses import dataclass
@dataclass
class BadClass:
items: list = [] # BAD — all instances share the same list
a = BadClass()
b = BadClass()
a.items.append(1)
print(b.items) # [1] — unexpected! b sees a mutation
原因:与函数参数的可变默认值问题同源——[] 在类定义时被创建一次,所有实例共享。
解决:使用 field(default_factory=list):
from dataclasses import dataclass, field
@dataclass
class GoodClass:
items: list = field(default_factory=list)
a = GoodClass()
b = GoodClass()
a.items.append(1)
print(b.items) # [] — each instance gets its own independent list
warning
错误 2:比较 frozen dataclass 时混用类型
from dataclasses import dataclass
@dataclass(frozen=True)
class A:
x: int
@dataclass(frozen=True)
class B(A):
y: int
a = A(10)
b = B(10, 20)
print(a == b) # False — type() is checked first
原因:dataclass 的 __eq__ 实现会先比较 type(self) == type(other),再逐项比较字段值。即使子类包含父类的所有字段,不同类型的实例也永远不会相等。
解决:这是正确行为——不同类型的实例本不应相等。需要值相同则显式提取所需字段比较,或不要将两个不同类做等同判断。
最佳实践
- 数据容器优先使用
@dataclass— 比手写__init__、__repr__更简洁,比 namedtuple 更灵活,是现代 Python 的标准实践 - 可变默认值一律用
default_factory—list、dict、set等可变类型永远不要直接赋为默认值,始终用field(default_factory=...) - 配置类和无共享状态对象用
frozen=True— 不可变性消除意外修改的 bug,使实例可哈希(能放入set或作dict的键),也更利于类型检查工具推断
练习
- 使用
@dataclass定义一个Book类,包含字段:title(str)、author(str)、isbn(str)、price(float)、tags(List[str] 默认空列表)。创建一个实例并打印其字符串表示。
查看答案
from dataclasses import dataclass, field
from typing import List
@dataclass
class Book:
title: str
author: str
isbn: str
price: float
tags: List[str] = field(default_factory=list)
book = Book("Learning Python", "Mark Lutz", "978-1449355739", 59.99, ["Python", "Programming"])
print(book)
# Book(title='Learning Python', author='Mark Lutz', isbn='978-1449355739', price=59.99, tags=['Python', 'Programming'])
- 定义一个不可变 dataclass
Vector2D(x: float,y: float),实现__add__魔术方法使其支持v1 + v2返回新的 Vector2D 实例,并添加一个magnitude属性方法计算模长。
查看答案
from dataclasses import dataclass
import math
@dataclass(frozen=True)
class Vector2D:
x: float
y: float
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
@property
def magnitude(self) -> float:
return math.sqrt(self.x ** 2 + self.y ** 2)
v1 = Vector2D(3.0, 4.0)
v2 = Vector2D(1.0, 1.0)
v3 = v1 + v2
print(v3) # Vector2D(x=4.0, y=5.0)
print(v3.magnitude) # 6.4031242374328485
知识检查
-
@dataclass装饰器默认不会生成以下哪个方法?- A.
__init__ - B.
__repr__ - C.
__hash__ - D.
__eq__
- A.
-
为 dataclass 字段设置可变类型(如
list)的默认值时,正确做法是?- A.
items: list = [] - B.
items: list = field(default=[]) - C.
items: list = field(default_factory=list) - D.
items: list = field(init=False)
- A.
-
以下关于
frozen=Truedataclass 的描述,正确的是?- A. 字段可以修改但无法添加新字段
- B. 实例赋值任何字段都会触发
FrozenInstanceError - C. 只能通过
__post_init__设置字段值,其他方法不行 - D.
frozen=True不影响__eq__的比较行为
查看答案
- C —
@dataclass默认生成__init__、__repr__、__eq__;__hash__仅在frozen=True或unsafe_hash=True时生成 - C —
default_factory=list每次实例化调用list()创建独立新对象,避免共享可变默认值 - B —
frozen=True使实例完全不可变,任何字段赋值(包括__post_init__之外的任何位置)都会触发FrozenInstanceError
本章小结
@dataclass自动为带类型注解的类生成__init__、__repr__、__eq__等样板方法field(default_factory=...)解决可变默认值共享问题,是每个 dataclass 使用者的必备知识frozen=True创建不可变实例,消除意外修改并自动支持哈希__post_init__钩子在自动__init__后执行,适合字段校验和计算字段赋值- 数据类继承遵循"父类字段优先"规则,子类扩展父类的同时保留 auto-generated 方法
术语表
| 英文 | 中文 | 说明 |
|---|---|---|
| dataclass | 数据类 | 使用 @dataclass 装饰器自动样板方法的类 |
| boilerplate | 样板代码 | 大量重复的手写方法(__init__、__repr__ 等) |
default_factory | 默认工厂函数 | 用于生成可变默认值的无参可调对象 |
frozen=True | 冻结/不可变 | 使 dataclass 实例不可修改字段的配置 |
__post_init__ | 后初始化钩子 | 在自动生成的 __init__ 末尾执行的自定义逻辑 |
| named tuple | 命名元组 | 带字段名的不可变 tuple 子类 |
下一步
- 阶段复习:进阶部分 → 回顾整个进阶部分的知识,检验学习成果
源码链接
- 数据类是 Python 标准库模块,无需额外安装:
from dataclasses import dataclass, field