7.3 类型注解与静态检查


7.3 类型注解与静态检查

本节摘要:类型注解是写给人和工具看的独立信息层——解释器在运行时几乎不检查它。本节讲注解语法全景(函数、变量、容器、Optional/Union)、typing 工具箱的常用件、mypy 的接入方式,以及"运行时不强制"边界的两种工程应对:pydantic 运行时校验与 TypeVar 泛型。

一个必须先说清的边界

先做个实验,校准预期:

def add(a: int, b: int) -> int: return a + b add("1", "2") # 注解说 int,实际传 str '12' # 照样跑通!

注解在运行时只是被存进 add.__annotations__ 的一堆对象,解释器不做任何强制。它的价值全在"运行之前":IDE 的补全与重构、mypy/pyright 的静态检查、文档化的函数签名。所以准确的定位是:Python 的类型系统是一个可选的、事后补上的静态层。这既是它的灵活(渐进式采用、按文件开关),也是它的边界(想挡住脏数据得靠运行时校验,见本节末)。

注解语法全景

函数注解从 3.5 起标准化,容器内类型从 3.9 起直接用内置名:

def top_n(scores: dict[str, int], n: int = 5) -> list[tuple[str, int]]: return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:n] # 变量注解:声明与意图 lang: str = "python" cache: dict[str, float] = {} # 可选与多选:老写法 Optional / Union,3.10 起用 | 更顺眼 def find_user(uid: int) -> dict | None: ... # 函数式参数(Callable)与结构化(序列化风格) from collections.abc import Callable, Sequence, Iterable def apply(fn: Callable[[int, int], int], xs: Sequence[int]) -> list[int]: return [fn(x, x) for x in xs]

几个高频工具件的用途速记:

  • Any:明确放弃检查的逃生门(少用,用多了类型系统名存实亡);
  • Iterable vs list:参数尽量收宽(Iterable),返回尽量给窄(list)——既不逼调用方转换,又承诺明确结构;
  • TypedDict:dict 的键结构描述,JSON 形状的事实标准;
  • Literal:值受限(如 mode: Literal["r", "w"]),比枚举轻;
  • Protocol:静态版的鸭子类型(4.2 节的续篇)——有这些方法就算实现,不用继承:
from typing import Protocol class Speaks(Protocol): def speak(self) -> str: ... def announce(x: Speaks) -> str: return x.speak() class Robot: # 不继承 Speaks,但结构满足 def speak(self) -> str: return "哔哔" announce(Robot()) # mypy 判定合法——静态鸭子类型

mypy:让注解长出牙齿

没有检查器的注解只是装饰。mypy 的最小接入:

$ python -m pip install mypy $ mypy --strict app.py app.py:12: error: Argument 1 to "top_n" has incompatible type "dict[str, str]"; expected "dict[str, int]" [arg-type] Found 1 error in 1 file (checked 1 source file)

报告格式与 traceback 同款风格:位置 + 双方类型 + 错误类别。--strict 全开适合新项目;存量代码用逐目录放宽的策略(在配置文件里按目录设 ignore_errors)。实践节奏建议:先给公共 API 与边界(入口函数、数据模型)上注解,内部实现逐步跟进——收益 80% 来自边界处的契约明确。

dataclass(4.1 节)与注解是天作之合:字段声明即类型契约,pyright/mypy 直接检查构造调用:

from dataclasses import dataclass @dataclass(frozen=True) class OrderLine: sku: str qty: int price: float @property def total(self) -> float: return self.qty * self.price lines = [OrderLine("A1", 2, 9.9), OrderLine("B2", 1, 19.5)] sum(l.total for l in lines) # 39.3

泛型:TypeVar 与新式 type 语句

写一个"进什么类型、出什么类型"的函数,需要类型变量:

def first(xs: list[int]) -> int: ... # 只服务 int,太窄 def first(xs: list) -> Any: ... # 万能但无检查,太松 from typing import TypeVar T = TypeVar("T") def first(xs: list[T]) -> T: # 类型随调用点具象化 return xs[0] first([1, 2]) + 1 # 推断为 int,合法 first(["a"]) .upper() # 推断为 str,合法

3.12 起的新语法把 TypeVar 声明下沉到函数签名(def first[T](xs: list[T]) -> T),方向一致、写法更省。类型系统本身也在"渐进演化",这也是为什么类型注解生态建议锁版本——工具间对新语法的支持有先后。

运行时校验:边界处的另一半

静态检查管不住外部输入(JSON、表单、环境变量)。信任边界上用 pydantic 把注解变成运行时约束:

from pydantic import BaseModel, field_validator class UserIn(BaseModel): uid: int name: str score: float = 0.0 @field_validator("name") @classmethod def no_blank(cls, v: str) -> str: if not v.strip(): raise ValueError("姓名不能为空白") return v UserIn.model_validate({"uid": "7", "name": "Ann"}) # "7" 被强制转成 int # UserIn(uid=7, name='Ann', score=0.0) UserIn.model_validate({"uid": "x", "name": "Ann"}) # ValidationError: 输入不是合法整数(pydantic 报出字段与原因)

注意 "7" 被自动强转——pydantic 默认是"尽力 coerce"而不是"严格拒绝",要不要更严可以按字段配置。FastAPI(第 9 章)的请求模型就是这套机制的直通车。

⚠️ 常见坑:以为注解会拦住脏数据(运行时不检查);list[int] 写在 3.8 环境上跑(旧版本需 typing.List);循环导入因类型注解 import 挑起——用 if TYPE_CHECKING: 守卫(5.1 节的延迟导入变体);mypy 报了错就全局 ignore,掩盖真问题。

💡 关键直觉:类型注解是"给读者的契约、给工具的任务清单"。合同写在边界(公共 API、数据模型、外部输入),利润最高。

类型驱动开发的节奏

补一个把注解用出复利的日常工作流。第一步,给最外层的入口函数签名补全类型——输入从哪来(请求体、命令行参数、配置文件)就在哪里立契约,pydantic 模型把外部世界不可信的假设固化成代码。第二步,让类型检查器进入编辑环节而不是月底体检:现代编辑器装上类型检查扩展后,错误在保存时实时标红,修复成本是月底批量修的十分之一。第三步,用类型作为重构护栏——改一个函数签名后,所有调用点的报错就是你的待办清单,比全文搜索可靠得多,搜索会命中同名变量,类型检查不会。这套节奏跑顺之后会出现一个反直觉的事实:类型注解写得越细,需要的单元测试反而可以越聚焦——一大类传错参数的用例被检查期直接消灭,测试可以专注业务逻辑本身。

本节要点回顾

  • 边界先行:注解运行时不强制,价值在 IDE、mypy 与文档;边界处配 pydantic 做运行时校验。
  • 语法全景:容器内类型、| 可选、Callable、Literal、TypedDict、Protocol。
  • 宽进严出:参数收 Iterable,返回给 list;Any 是逃生门不是默认项。
  • Protocol = 静态鸭子类型:结构满足即合格,无需继承。
  • TypeVar 泛型:类型随调用点具象化,3.12 有更省的新语法。
  • 渐进采用:先公共 API 后内部实现,strict 留给新代码。

第 8 章回到解释器最热的机制争论——GIL:多线程为何救不了 CPU 密集任务,以及三种并发模型的真实分工。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U