(六)用数据类整理记录字段
1. 从重复的初始化赋值到 dataclass
书名、作者、完成状态、标签都要初始化,手写 self.字段 = 参数 会越来越重复。数据类根据字段注解生成初始化等常用方法:
# dataclass 读取字段注解;field 的默认工厂在每个实例初始化时调用。
from dataclasses import dataclass, field
@dataclass
class Book:
title: str
author: str = ""
finished: bool = False
tags: list[str] = field(default_factory=list)
def finish(self) -> None:
self.finished = True
first = Book("Python", author="小林")
second = Book("NumPy")
first.tags.append("入门")
first.finish()
print(first)
print(second.tags)输出 Book(title='Python', author='小林', finished=True, tags=['入门']),以及 []。这里没有手写 __init__,装饰器已经按字段建立它;默认还生成用于显示字段的 __repr__ 与按字段比较的 __eq__。
普通类中的 title: str 只是注解,不会自动保存一个标题值;@dataclass 额外读取这些注解来生成方法,也仍然不会自动检查运行时每个字段是否符合类型。
field(default_factory=list) 保存 list 这个可调用对象,等每次初始化再调用 list()。若提前写成 default_factory=list(),交进去的就是一个列表,不是创建动作。直接把可变列表写成数据类字段默认值,例如 tags: list[str] = [],这份定义会被数据类报告为错误。
2. 字段比较与对象身份
# 生成的相等比较读取字段值,is 仍然比较是否为同一个对象。
from dataclasses import dataclass
@dataclass
class Book:
title: str
finished: bool = False
first = Book("Python")
second = Book("Python")
print(first == second, first is second)
second.finished = True
print(first == second)输出 True False 与 False。默认生成的比较先要求实际类型相同,再按参与比较的字段计算结果。两份记录的值相同,并不表示它们的状态共享;修改 second 仍然只修改第二个实例。
默认的可变数据类不能直接作为集合元素或字典键:其字段比较结果可能随修改改变,数据类因此不提供对应的默认散列。给书籍分配稳定编号,或用书名等已经选定的值作为索引,就能把“查找位置”和“可变记录”分开。数据类行为
3. __post_init__ 与只读字段
需要根据初始化字段算出另一个值时,可以使用 __post_init__。生成的初始化方法先保存字段,再调用它:
# init=False 表示 ratio 不作为初始化参数,由后处理根据页数算出。
from dataclasses import dataclass, field
@dataclass
class ReadingSnapshot:
pages: int
total_pages: int
ratio: float = field(init=False)
def __post_init__(self):
if self.total_pages <= 0 or not 0 <= self.pages <= self.total_pages:
raise ValueError("页数记录错误")
self.ratio = self.pages / self.total_pages
record = ReadingSnapshot(50, 200)
print(record.ratio)输出 0.25。这是建立时形成的快照字段,后来修改 record.pages 并不会重新调用 __post_init__。需要每次读取都跟着当前值变化时,使用前面的 property。
数据类的 frozen=True 可以阻止普通的字段重新赋值,但它不递归改变字段所指对象:
# frozen 阻止重绑 title;标签列表本身仍然是可变对象。
from dataclasses import FrozenInstanceError, dataclass, field
@dataclass(frozen=True)
class BookInfo:
title: str
tags: list[str] = field(default_factory=list)
info = BookInfo("Python")
info.tags.append("入门")
print(info.tags)
try:
info.title = "NumPy"
except FrozenInstanceError:
print("字段不能重新赋值")得到 ['入门'] 和“字段不能重新赋值”。FrozenInstanceError 是数据类提供的错误类型。希望标签内容也作为固定快照时,可以把它保存成元组;对象的可变性仍要沿着引用关系看。数据类设计
4. 数据类中的类属性
# ClassVar 告诉数据类与类型工具:category 是类配置,不是记录字段。
from dataclasses import dataclass, fields
from typing import ClassVar
@dataclass
class Book:
title: str
category: ClassVar[str] = "书籍"
book = Book("Python")
print(book.category)
print([item.name for item in fields(Book)])输出“书籍”和 ['title']。fields(Book) 提供数据类字段信息,ClassVar 标记的名称不进入生成的初始化参数与字段比较。普通实例仍然可以按前面讲的类属性规则读取 category;ClassVar 是这份注解的含义说明,不是运行时禁止实例赋值的开关。