Blog

Python 数据类(dataclass)

一个装饰器就能替你写好 __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 得到只读、可哈希的实例。
  • asdictreplace 分别负责序列化和更新。

如果你还需要在程序边界做校验和解析,那正是 Pydantic 的用武之地。如果只是内部用来装数据,dataclass 就够了,而且没有任何额外成本。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。