Python 现代开发学习笔记

(六)用数据类整理记录字段

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 是这份注解的含义说明,不是运行时禁止实例赋值的开关。

笔记正文与全部题解 · Ctrl / ⌘ K