>>>PyPathPython 学习站
首页›进阶篇›kp-026
进阶篇 · Advanced lv.3 进阶 kp-026

类型注解与静态检查

前置知识:kp-007、kp-020

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 x

Iterable/Iterator/Sequence 等抽象集合类型来自 collections.abc——接受宽、返回窄是注解设计的黄金法则(参数收 Iterable,返回 list)。

9. 常见误区

  1. 以为加了注解就安全了 —— 不跑检查器它只是注释;mypy/pyright 才是执行者。
  2. list 与 List 混用 —— 3.9+ 直接用内置小写 list[int];旧代码里的 typing.List 已过时。
  3. 过度精确 —— 到处写具体类型反而失去鸭子灵活性;接口处用 Iterable[T]、Sequence[T]。
  4. Callable 写成 callable —— 前者是类型,后者是内置函数;类型位置必须大写开头。

10. 自测题

  1. def f(x: int) 传入 "a" 会发生什么?谁会报错、什么时候报?
  2. int | None 的旧写法是什么?3.10 之前怎么写联合类型?
  3. Protocol 与继承式多态的取舍?
参考答案
  1. 运行时正常执行(除非函数体自己炸);mypy/pyright 在静态检查时报错。
  2. Optional[int];3.10 之前用 Union[int, None]。
  3. 不想强加继承关系、只想约束"有这些方法"时用 Protocol(结构化);需要共享实现/运行时注册机制时用 ABC/继承。

11. 与其他知识点的关系

  • kp-040 类型系统前沿:PEP 695 语法、Self、TypeVarTuple。
  • kp-020 dataclass:注解驱动的数据建模。
  • kp-045 工程规范:类型检查进 CI。

12. 延伸阅读

  • mypy 官方文档:https://mypy.readthedocs.io/
  • Python typing 速成:https://typing.readthedocs.io/