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 看的」,而是系统的输入契约。写注解的习惯从这里开始就有复利。

工程纪律四条

  1. 公共函数必须注解(签名是 API),内部小函数随意。
  2. 返回值比参数更重要——调用方靠它决定下一步,-> None 也是信息(「这函数纯副作用」)。
  3. 不追求 100% 覆盖。动态到无法注解的地方(鸭子类型密集区),Any 是合法出口,但要在旁边注释为什么。
  4. 注解错了比不写更糟——检查器信了错误注解会放过真 bug。重构后同步更新注解,当成代码的一部分。

自检三题

  1. Optional[int]、int | None、Union[int, None] 三者的关系?
  2. 为什么 TaskIn(title=123) 在运行时抛异常,而普通函数的错误注解运行时不报?(两种机制的本质区别。)
  3. Callable[[str], bool] 描述的是什么?写一个符合它的函数签名。

课程收束

到这里,essentials 的体系走完了。回头看你已经握住的主线:

下一站:python-yield-deepdive——从 07 章的协议出发,把 yield 拆到最底层。

进入 keel 阅读