Python 是动态类型语言,但今天任何一个稍具规模的工程,几乎都离不开类型注解。问题在于很多人停留在"加了 : int 显得专业"的层面,没搞清楚:注解在运行时到底有没有用?mypy 是怎么在不运行代码的情况下判断类型对不对的?List 和 list 有什么区别?这些才是把类型用对、用好的关键。
场景:注解在运行时被无视
先记住一个反直觉的事实——类型注解默认不影响运行时行为:
1 | def add(a: int, b: int) -> int: |
注解不是断言,解释器不会用它做类型检查。它的真实身份是元数据,存在 __annotations__ 里:
1 | print(add.__annotations__) # {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>} |
这就引出类型系统的核心定位:注解是给静态分析工具和人看的,不是给解释器看的。真正去检查 add("x", "y") 不合法的,是 mypy / pyright 这类独立工具,它们在你提交代码前、不运行程序的情况下完成检查。
机制:静态检查器如何推断
mypy 做的事叫"类型推断 + 类型检查"。它解析 AST,为每个表达式赋一个静态类型,再验证赋值、调用、返回是否兼容。两个支撑它的核心理论概念值得理解:
渐进类型(Gradual Typing)。 Python 允许部分注解、部分不注解。未注解的地方被赋予特殊类型 Any,Any 与任何类型都兼容——它是检查器和动态世界之间的"逃生舱"。这让你能给老代码逐步加注解而不必一次到位。但代价是:Any 会"传染",一旦某个值是 Any,依赖它的推断都会松掉,检查形同虚设。滥用 Any 等于关掉类型检查。
协变与逆变(Variance)。 这是最容易踩坑的地方。List[Dog] 是不是 List[Animal] 的子类型?答案是不是。因为 list 可变:如果允许把 List[Dog] 当 List[Animal] 用,别人就能往里塞一只 Cat,破坏了原 list 的不变量。所以可变容器是不变的(invariant)。而只读的 Sequence[Dog] 可以当 Sequence[Animal] 用,它是协变的(covariant)。这解释了一条实用准则:
1 | from collections.abc import Sequence, Iterable |
源码:常用注解的演进
1 | from typing import Optional, Union |
现代 Python 已经可以直接用 list[int]、dict[str, int]、int | None,不必再从 typing 导入 List、Dict、Optional、Union。X | None 就是 Optional[X],二者等价,前者更直观。
TypeVar 让你写真正的泛型函数,保留输入输出的类型关联:
1 | from typing import TypeVar |
Protocol 则把 Python 的"鸭子类型"写进了类型系统——只要结构匹配就算兼容,不需要显式继承:
1 | from typing import Protocol |
这是结构化子类型(structural subtyping),契合 Python 一贯的"长得像就行"哲学,比强制继承基类优雅得多。
工程权衡与边界
- 运行时零成本,但不是零代价。 注解不拖慢运行(除非你用
get_type_hints反射),但维护注解、配置 mypy 严格度、处理误报都要投入。小脚本不值得,大项目和库则回报丰厚。 from __future__ import annotations。 开启后所有注解变成字符串延迟求值,解决前向引用(类引用自身、互相引用)和导入循环问题,也避免在模块加载时构造昂贵的注解对象。代价是运行时拿注解必须用typing.get_type_hints去解析字符串。- 严格度是连续光谱。 mypy 从默认的宽松到
--strict之间有十几个开关(disallow_untyped_defs、warn_return_any等)。新项目建议直接上较严格配置;老项目逐模块收紧,配合# type: ignore[code]精确压制而非全局放水。
常见误区
- 以为加了注解就有了运行时校验。 没有。要运行时校验得用 Pydantic、dataclass + 校验,或
typeguard这类库,它们才真正读注解做断言。 # type: ignore不写错误码。 裸# type: ignore会吞掉该行所有类型错误,包括以后新引入的真问题。永远写成# type: ignore[arg-type]限定范围。- 把
Any当object用。object是所有类型的顶,但对它几乎不能做任何操作(类型安全);Any则什么都能做(放弃检查)。需要"接受任意类型但仍安全"时用object加isinstance收窄,而不是Any。 - 可变默认值 + 注解的双重坑。
def f(x: list[int] = [])既有可变默认值陷阱,注解也帮不了你,用x: list[int] | None = None在体内初始化。
小结
类型注解的本质是"运行时无视、静态有用"的元数据:解释器不读它,mypy/pyright 读。要把它用好,得理解渐进类型里 Any 的逃生舱与传染性、可变容器的不变性、Protocol 带来的结构化鸭子类型。现代语法已经能用 list[int]、int | None 写得很干净。记住一句话——类型系统是工具不是枷锁,它的价值在于让大型代码库在重构和协作中"提前在编译期暴露错误",而不是装饰你的函数签名。