理解注解在运行时究竟做了什么,并借助 frozen、slots 和 default_factory 把 dataclasses 用到极致。
在 Kodokon 中打开本课类型注解是元数据,而不是约束:解释器把它们存放在 __annotations__ 里,运行时从不检查它们。检查是 mypy 或 pyright 这类静态分析器的工作,发生在执行之前。直接的后果是:一个类型写错的程序照样毫无怨言地运行 - 而这正是有意为之的设计。渐进式类型让你能够一点一点地为现有代码库添加注解,而不破坏任何东西。
from typing import get_type_hints
def add(a: int, b: int) -> int:
return a + b
print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))从 Python 3.9 开始,内置类型本身就是泛型:直接写 list[int] 或 dict[str, float],无需导入 typing.List。从 3.10 起,联合类型写作 int | None。要把输入类型和输出类型绑定起来,就使用 TypeVar - Python 3.12 甚至提供了紧凑写法 def first[T](items: list[T]) -> T。有一个常被忽视的点:from __future__ import annotations 会让所有注解变成惰性的(以字符串形式存储),这既允许你引用在后面才定义的类,也降低了导入时的开销。
from collections.abc import Iterable, Iterator
from typing import TypeVar
T = TypeVar("T")
def dedupe(items: Iterable[T]) -> Iterator[T]:
seen: set[T] = set()
for item in items:
if item not in seen:
seen.add(item)
yield item
print(list(dedupe([3, 1, 3, 2, 1])))from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
@dataclass
class Basket:
items: list[str] = field(default_factory=list)
total: int = 0
def __post_init__(self) -> None:
self.total = len(self.items)
point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))有三个参数能把 @dataclass 变成一件生产级的工具。frozen=True 让实例变得不可变,并结合自动生成的相等性判断使它们可哈希 - 可以用作字典的键。slots=True(Python 3.10+)移除了每个实例的 __dict__:内存占用更低,属性访问更快,任何意料之外的属性赋值都会失败。最后,kw_only=True 强制调用时显式使用关键字参数。__post_init__ 则是安放不变式和派生字段的地方。对于一个简单的不可变聚合体,NamedTuple 依然说得通;一旦涉及行为或校验,就优先选择 dataclass。
(a: int, b: int) -> int,而你调用 add('py', 'thon') 时,运行时会发生什么?items: list[str] = [] 不能作为 dataclass 字段的默认值?list[str] 不是字段的合法注解frozen=True 对一个 dataclass 做了什么?