进阶篇 · Advanced
lv.3 进阶
kp-026
类型注解与静态检查
1. 一句话定义
类型注解是写给静态检查器看的类型声明(运行时默认不校验):def f(x: int) -> str;配合 mypy/pyright 在编码期抓住类型错误,动态语言的开发速度与静态语言的可维护性兼得。
2. 为什么重要
项目过千行后,注解的价值曲线陡升:IDE 补全、重构信心、接口契约。它是现代 Python 的默认工程实践(新项目建议直接开启严格模式),也是读懂 2020 年代第三方库 API 的前提。
3. 前置知识
kp-007(函数)、kp-020(dataclass 字段即注解)。
4. 核心概念
python
# 内置泛型(3.9+ 直接用小写)
def mean(xs: list[float]) -> float: ...
def lookup(d: dict[str, int], k: str) -> int | None: ... # 3.10+ 的 | 联合
from typing import Callable, Protocol, TypedDict, Literal, TypeAlias
Handler: TypeAlias = Callable[[str], bool] # 函数类型
class Shape(Protocol): # 结构化子类型(鸭子类型的静态版)
def area(self) -> float: ...
def total(shapes: list[Shape]) -> float: # 任何有 area() 的对象都算 Shape
return sum(s.area() for s in shapes)
class Point(TypedDict):
x: int
y: int
def move(mode: Literal["up", "down"], p: Point) -> Point: ...- Optional 与 None:
int | None等价旧写法Optional[int]。 - 泛型函数/类:
def first[T](xs: list[T]) -> T(3.12+ 新语法,见 kp-040)。
5. 原理与机制
运行时不强制:def f(x: int) 传字符串照跑——注解只是元数据(存进 __annotations__,框架如 Pydantic 会读它做校验,kp-020)。强制力来自编辑器内的静态检查:
bash
uvx mypy src/ # 或 pip install mypy 后 mypy src/
uvx ruff check src/ # lint 与类型互补渐进式类型(gradual typing):可以只给公共接口加注解、内部逐步覆盖;--strict 全量收紧。
6. 关键事实(模型/图示)
text
写注解 → IDE/检查器推导 → 提前报错(编码期)
↘ 框架读取(Pydantic 校验、FastAPI 文档生成)
运行时仍按动态语言执行(除非框架显式校验)Protocol 与 ABC 的分工:Protocol 看形状(有没有这方法),ABC 看血统(是否继承);优先 Protocol(kp-018 的组合哲学在类型层的延伸)。
7. 直观类比
注解像菜谱上的分量标注:盐: 5g——你(运行时)倒多倒少没人拦,但看菜谱的人(检查器)会立刻指出"这步要放 5 克不是 5 斤"。Pydantic 则是"倒之前真的会过秤"的厨房。
8. 实例与案例
python
# 修复一个经典类型问题:可变参数的泛型
from collections.abc import Iterable, Iterator
def dedupe[T](items: Iterable[T]) -> Iterator[T]:
seen: set[T] = set()
for x in items:
if x not in seen:
seen.add(x)
yield xIterable/Iterator/Sequence 等抽象集合类型来自 collections.abc——接受宽、返回窄是注解设计的黄金法则(参数收 Iterable,返回 list)。
9. 常见误区
- 以为加了注解就安全了 —— 不跑检查器它只是注释;
mypy/pyright才是执行者。 list与List混用 —— 3.9+ 直接用内置小写list[int];旧代码里的typing.List已过时。- 过度精确 —— 到处写具体类型反而失去鸭子灵活性;接口处用
Iterable[T]、Sequence[T]。 Callable写成callable—— 前者是类型,后者是内置函数;类型位置必须大写开头。
10. 自测题
def f(x: int)传入"a"会发生什么?谁会报错、什么时候报?int | None的旧写法是什么?3.10 之前怎么写联合类型?- Protocol 与继承式多态的取舍?
参考答案
- 运行时正常执行(除非函数体自己炸);mypy/pyright 在静态检查时报错。
Optional[int];3.10 之前用Union[int, None]。- 不想强加继承关系、只想约束"有这些方法"时用 Protocol(结构化);需要共享实现/运行时注册机制时用 ABC/继承。
11. 与其他知识点的关系
12. 延伸阅读
- mypy 官方文档:https://mypy.readthedocs.io/
- Python typing 速成:https://typing.readthedocs.io/