数据类 dataclass
从 Python 3.7 起,dataclasses 模块提供了一种简洁的方式来创建主要用于存储数据的类。数据类通过装饰器自动生成特殊方法,避免了手写大量样板代码。在 Python 3.12 中,数据类与类型注解体系结合得更加紧密。
基本用法
使用 @dataclass 装饰器,配合类型注解声明字段,解释器会自动生成 __init__、__repr__ 和 __eq__ 等方法:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
print(p.x) # 1.0
对比传统类定义,数据类省去了手写 __init__ 和赋值语句的繁琐:
# 传统写法
class PointOld:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"PointOld(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, PointOld):
return NotImplemented
return self.x == other.x and self.y == other.y
字段默认值
数据类字段可以指定默认值,有默认值的字段必须放在无默认值字段之后:
from dataclasses import dataclass
@dataclass
class Config:
name: str
debug: bool = False
timeout: int = 30
c = Config("server")
print(c) # Config(name='server', debug=False, timeout=30)
如果默认值是可变对象(如列表或字典),必须使用 field(default_factory=...),否则所有实例会共享同一个可变对象:
from dataclasses import dataclass, field
@dataclass
class Item:
name: str
tags: list = field(default_factory=list)
a = Item("apple")
b = Item("banana")
a.tags.append("fruit")
print(b.tags) # [],各自独立
直接使用 tags: list = [] 会导致所有实例共享同一个列表,这是与函数默认参数类似的陷阱。
field() 的更多选项
field() 函数可以精细控制字段行为:
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
price: float = field(compare=False) # 不参与比较
quantity: int = field(default=0, repr=False) # 不显示在 repr 中
_internal_id: int = field(init=False, repr=False) # 不由 __init__ 接收
def __post_init__(self):
self._internal_id = hash(self.name)
__post_init__ 方法在自动生成的 __init__ 执行完毕后被调用,适合进行字段校验或派生值计算。
自动生成的方法
@dataclass 的 eq、order、frozen 等参数控制自动生成哪些方法:
from dataclasses import dataclass
@dataclass(order=True)
class Student:
name: str
score: int
s1 = Student("Alice", 85)
s2 = Student("Bob", 92)
print(s1 < s2) # True,按字段顺序比较(先比 name,再比 score)
启用 order=True 会自动生成 __lt__、__le__、__gt__、__ge__。比较按字段定义的顺序逐字段进行。如果只想按特定字段排序,可以配合 field(compare=False) 屏蔽无关字段。
frozen=True 创建不可变数据类,实例创建后任何修改都会触发 FrozenInstanceError:
from dataclasses import dataclass
@dataclass(frozen=True)
class ImmutablePoint:
x: float
y: float
p = ImmutablePoint(1.0, 2.0)
# p.x = 3.0 # dataclasses.FrozenInstanceError
不可变数据类的实例可以作为字典的键或放入集合中。
继承与数据类
数据类支持继承,子类可以添加新字段或覆盖父类字段的默认值:
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
employee_id: str
department: str = "未分配"
e = Employee("张三", 30, "E001")
print(e) # Employee(name='张三', age=30, employee_id='E001', department='未分配')
继承时,子类的字段排在父类字段之后,__init__ 的参数顺序也遵循这一规则。
转换为字典或元组
数据类实例可以方便地转换为字典或元组,便于序列化:
from dataclasses import dataclass, asdict, astuple
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(asdict(p)) # {'x': 1.0, 'y': 2.0}
print(astuple(p)) # (1.0, 2.0)
与 match-case 结合
Python 3.10+ 的 match 语句可以直接匹配数据类实例,按字段解构:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(0, 5)
match p:
case Point(x=0, y=y):
print(f"Y轴上的点,y={y}")
case Point(x=x, y=0):
print(f"X轴上的点,x={x}")
case Point(x=x, y=y):
print(f"普通点 ({x}, {y})")
与 typing.NamedTuple 和普通类的选择
| 方式 | 可变 | 自动生成方法 | 适用场景 |
|---|---|---|---|
dataclass | 默认可变,frozen=True 可变 | __init__, __repr__, __eq__ 等 | 大多数数据存储场景 |
typing.NamedTuple | 不可变 | 继承自 tuple,有索引访问 | 需要不可变且轻量 |
| 普通类 | 完全自定义 | 手动编写 | 复杂行为,非纯数据 |
数据类在纯数据存储场景下是首选,它用最小的代码量提供了最大的便利性。
常见错误
忘记类型注解:
# 错误:没有类型注解,dataclass 不会将其识别为字段
@dataclass
class Bad:
x = 0 # 这是类属性,不是数据类字段
# 正确
@dataclass
class Good:
x: int = 0
可变默认值的陷阱:
from dataclasses import dataclass
# 错误:所有实例共享同一个列表
@dataclass
class BadItem:
tags: list = []
# 正确:使用 default_factory
from dataclasses import field
@dataclass
class GoodItem:
tags: list = field(default_factory=list)
有默认值的字段位置:
# 错误:有默认值的字段不能放在无默认值字段前面
# @dataclass
# class Bad:
# x: int = 0
# y: int # TypeError
# 正确
@dataclass
class Good:
y: int
x: int = 0
数据类是 Python 3.7+ 中处理结构化数据的首选工具。它通过减少样板代码,让开发者专注于数据本身而非类的机械构造。