KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
08 · 类型注解:写给谁看的代码 — keel 龙骨
## 这一章解决什么问题
这一章解决什么问题
类型注解在 Python 里不影响运行——运行时它们基本被忽略。那为什么值得写?这一章回答三个问题:注解写给谁看(答案:IDE、类型检查器、以及三个月后的你自己);现代写法长什么样(3.10+/3.12 的新语法比老教程简洁得多);注解在哪些场景从文档变成了功能。
最小可用集
def price(total: float, count: int = 1) -> float:
return total * count
name: str = "python"
ids: list[int] = [1, 2, 3]
cache: dict[str, int] = {}
参数: 类型、-> 返回类型、变量: 类型。就这么多语法。运行时完全不管对错——price("a", "b") 照样跑,炸不炸看运算符。检查发生在工具侧:
pip install mypy
mypy your_module.py # 静态检查,不用运行代码就能发现类型错误
IDE 的补全、跳转、参数提示全部建立在注解上——这是多数人写注解的第一动机,也是最实在的回报。
现代写法(3.10+)
老教程里的一堆 import 大多可以扔了:
# 旧写法 # 3.10+ 现代写法
from typing import List # List[int] → list[int]
from typing import Dict # Dict[str, int] → dict[str, int]
from typing import Optional # Optional[int] → int | None
from typing import Union # Union[A, B] → A | B
| 读作「或」:
def find_user(uid: int) -> dict | None: # 找不到返回 None —— 最常见的返回签名
...
def parse(raw: str | bytes) -> int: # 两种输入都接受
...
class Client:
timeout: float | None = None # 可以不配
None 检查是类型检查器最擅长的——你写了 find_user(...) 不判 None 就 .get(...),mypy 直接标红。把「可能为 None」写进签名,就是把契约显式化。
回调与可调用对象:
from collections.abc import Callable
def apply(fn: Callable[[int], str], x: int) -> str:
return fn(x)
# Callable[[参数类型列表], 返回类型]
泛型与 TypeAlias:描述「同类元素的容器」
# 自定义类型别名:给复杂签名起人话名字
type Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None
# 3.12 的 type 语句;旧版本用 TypeAlias
# 泛型函数:输入输出类型联动
def first(items: list[T]) -> T:
return items[0]
日常工程里 90% 的注解就是:list[...]、dict[str, ...]、X | None、Callable、自定义别名。再复杂的先不学,用到再查——注解的目标是可读,不是炫技。
注解什么时候从文档变成功能
两个真实场景,注解被运行时读取并执行:
1. dataclass(04 章你已经见过)——注解就是字段声明:
@dataclass
class TaskResult:
run_id: str # 这两行不是注释,是字段定义
attempts: int = 0
没有注解,dataclass 根本不知道哪些是字段。
2. Pydantic / FastAPI 类框架——注解在运行时被解析成校验规则和文档:
from pydantic import BaseModel
class TaskIn(BaseModel):
title: str
priority: int = 5
TaskIn(title="x", priority=99) # ✅
TaskIn(title=123) # ❌ ValidationError:框架读注解做了运行时校验
后续课的「请求模型与注册表」整章建立在 Pydantic 上——那时注解不再是「给 IDE 看的」,而是系统的输入契约。写注解的习惯从这里开始就有复利。
工程纪律四条
- 公共函数必须注解(签名是 API),内部小函数随意。
- 返回值比参数更重要——调用方靠它决定下一步,
-> None也是信息(「这函数纯副作用」)。 - 不追求 100% 覆盖。动态到无法注解的地方(鸭子类型密集区),
Any是合法出口,但要在旁边注释为什么。 - 注解错了比不写更糟——检查器信了错误注解会放过真 bug。重构后同步更新注解,当成代码的一部分。
自检三题
Optional[int]、int | None、Union[int, None]三者的关系?- 为什么
TaskIn(title=123)在运行时抛异常,而普通函数的错误注解运行时不报?(两种机制的本质区别。) Callable[[str], bool]描述的是什么?写一个符合它的函数签名。
课程收束
到这里,essentials 的体系走完了。回头看你已经握住的主线:
- 对象与可变(01)→ 解释了 dataclass 的 default_factory、frozen 的价值
- 函数是对象(02)→ 推出了闭包(03)、命令注册表、回调
- 类与协议(04)→ 魔法方法是 07 章两个协议的入口
- 异常是 API(05)→ 分层传播和 raise from 是所有后续工程代码的骨架
- import 三步骤(06)→ 单例、循环导入、延迟导入都有了机械解释
- 两个协议(07)→ for 和 with 不再是咒语,yield 课的大门已经打开
- 注解是契约(08)→ dataclass 与 Pydantic 的地基
下一站:python-yield-deepdive——从 07 章的协议出发,把 yield 拆到最底层。