一个装饰器就能替你写好 __init__、__repr__ 和 __eq__。本文讲默认值、冻结实例、排序、__post_init__,以及 default_factory 的规则。
数据类(dataclass)替你写那些枯燥的方法。你用类型注解声明字段,装饰器就会生成 __init__、__repr__ 和 __eq__。
从 Python 3.7 起它就在标准库里了,不用安装任何东西。
省掉的样板代码
下面的旧式类和后面的 dataclass 做的是同一件事。
# the boilerplate a dataclass removes
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):
return isinstance(other, PointOld) and (self.x, self.y) == (other.x, other.y)
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print(p == Point(1, 2))
输出:
Point(x=1, y=2)
True
相等比较的是字段,不是标识。对值对象来说,这几乎总是你想要的。
默认值和列表规则
简单的默认值照常写。可变默认值需要用 default_factory,它会为每个实例调用一次。
# defaults, and why lists need default_factory
@dataclass
class Basket:
owner: str
items: list = field(default_factory=list)
currency: str = 'INR'
b1, b2 = Basket('ada'), Basket('grace')
b1.items.append('apple')
print(b1)
print(b2)
try:
@dataclass
class Broken:
items: list = []
except ValueError as err:
print(type(err).__name__ + ':', err)
输出:
Basket(owner='ada', items=['apple'], currency='INR')
Basket(owner='grace', items=[], currency='INR')
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
有用的是最后几行。Python 直接拒绝可变默认值,而不是让所有实例共享同一个列表——普通函数参数就会出现那种共享。
冻结实例
frozen=True 让字段变成只读,实例也变得可哈希,于是可以当字典的键,也能放进集合。
# frozen instances are hashable and read only
@dataclass(frozen=True)
class Config:
host: str
port: int = 8080
c = Config('localhost')
print(c, hash(c) == hash(Config('localhost')))
try:
c.port = 9090
except Exception as err:
print(type(err).__name__ + ':', err)
输出:
Config(host='localhost', port=8080) True
FrozenInstanceError: cannot assign to field 'port'
排序
order=True 会生成比较方法,按字段声明的顺序逐个比较,和元组的比较方式一样。
# order gives you comparisons and sorting
@dataclass(order=True)
class Version:
major: int
minor: int
versions = [Version(1, 4), Version(1, 2), Version(0, 9)]
print(sorted(versions))
print(Version(1, 4) > Version(1, 2))
输出:
[Version(major=0, minor=9), Version(major=1, minor=2), Version(major=1, minor=4)]
True
排序时不需要 key 函数。
用 post_init 计算派生值
field(init=False) 让字段不出现在构造函数里,__post_init__ 则紧接着构造函数执行。
# __post_init__ for derived values
@dataclass
class Rect:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
print(Rect(3, 4))
输出:
Rect(width=3, height=4, area=12)
asdict 和 replace
asdict 递归地转换成普通字典。replace 新建一个实例并修改其中部分字段,冻结实例就是这样“更新”的。
# asdict and replace
print(asdict(Basket('ada', ['apple'])))
print(replace(Config('localhost'), port=9090))
输出:
{'owner': 'ada', 'items': ['apple'], 'currency': 'INR'}
Config(host='localhost', port=9090)
让字段不参与 repr 或相等比较
适合密钥、缓存以及任何干扰输出的东西。
# fields you do not want in repr or comparison
@dataclass
class User:
name: str
token: str = field(repr=False, compare=False)
print(User('ada', 'secret-token'))
print(User('ada', 'secret-token') == User('ada', 'different-token'))
输出:
User(name='ada')
True
token 不会打印出来,也不影响相等比较。
要点
- 装饰器根据带注解的字段生成
__init__、__repr__和__eq__。 - 可变默认值需要写成
field(default_factory=list)。 frozen=True得到只读、可哈希的实例。asdict和replace分别负责序列化和更新。
如果你还需要在程序边界做校验和解析,那正是 Pydantic 的用武之地。如果只是内部用来装数据,dataclass 就够了,而且没有任何额外成本。