前言
题面里「最新整理」四个字是关键------如果只是把变量和基础类型再讲一遍,那和十年前的教程没有区别。真正需要「最新」的,是类型注解这套机制 :从 3.5 引入 typing,到 3.9 支持内置泛型,3.10 支持 X | Y 联合写法,3.12 引入 type 语句和类型参数语法,再到 3.14 把注解的延迟求值变成默认行为(PEP 649 / PEP 749),这条线一直在变。用旧写法写新代码,轻则被类型检查器抱怨,重则踩到求值时机的坑。
最普遍的误解只有一个,但杀伤力很大:以为加了类型注解,Python 就会在运行时帮你检查类型。不会。注解默认只是「给工具看的元数据」,运行时它既不校验也不强制转换。传错类型的实参,程序照跑不误,除非你额外用了某个会在运行时读注解并校验的库。
本文讲三件事:注解到底是什么、现代写法该怎么写、以及 3.14 的延迟求值改变了什么。示例会明确标注每个特性所需的 Python 版本,并给出旧版本的替代写法。
一、注解是什么,不是什么
注解(annotation)是写在冒号后面的表达式,可以作用在变量、函数参数、函数返回值、类属性上。
它不是 :运行时的类型检查、类型转换、接口约束。它是:给类型检查器(mypy、pyright 之类)、IDE、以及文档工具消费的结构化元数据。
python
# 适用于 Python 3.8+
def greet(name: str, times: int = 1) -> str:
return ("你好," + name + "!") * times
print(greet("小明", 2)) # 你好,小明!你好,小明!
print(greet(123)) # 不报错!注解不拦截错误类型
最后一行能跑通,正是「注解不检查」的证据。真正想做运行时校验,得自己写判断,或者用第三方库------但那已经超出标准库范围了。
注解能被程序读到:函数的注解放在 __annotations__ 里,模块级变量的注解放在模块的 __annotations__ 里。
二、typing 模块:复杂类型怎么标注
基础类型直接用内置名字就行:int、str、float、bool、bytes、list、dict。但「列表里装的是字符串」这种信息,光写 list 表达不出来,需要泛型写法。历史上要用 typing 模块里的别名:
| 场景 | 老写法(typing) | 现代写法 | 现代写法所需版本 |
|---|
|-------|-------------|-------------|------|
| 字符串列表 | List[str] | list[str] | 3.9+ |
|----|------------------|------------------|------|
| 字典 | Dict[str, int] | dict[str, int] | 3.9+ |
|------|-------------------|-------------------|------|
| 元组定长 | Tuple[int, str] | tuple[int, str] | 3.9+ |
|----|------------|------------|------|
| 集合 | Set[int] | set[int] | 3.9+ |
|------|-------|------------------|------|
| 任意类型 | Any | Any(仍需 typing) | 3.5+ |
|---------|-----------------|----------|--------|
| 可为 None | Optional[int] | `int \ | None` |
|------|-------------------|----------|-------|
| 联合类型 | Union[int, str] | `int \ | str` |
|-----|------------------------|----------------------------------------|------|
| 可调用 | Callable[[int], str] | collections.abc.Callable[[int], str] | 3.9+ |
Optional[int] 只是 Union[int, None] 的简写,不代表参数可以省略 ------「可以省略」要靠默认值表达。Any 表示「放弃检查」,不是「任意类型都安全」,标了 Any 就等于放弃了这一处的类型保障,能少用就少用。
python
# 适用于 Python 3.10+
from typing import Any
def total_score(scores: dict[str, int]) -> int:
return sum(scores.values())
def find_user(uid: int) -> dict[str, Any] | None:
data = {1: {"name": "小明"}, 2: {"name": "小红"}}
return data.get(uid)
print(total_score({"语文": 90, "数学": 95})) # 185
print(find_user(1))
print(find_user(99)) # None
如果运行环境是 3.9 或更早,dict[str, int] 里只有最外层写法可换成 Dict,| 也要换成 Optional[...]。
三、版本演进:从 3.9 到 3.12
- 3.9 :内置容器类型支持下标,可以写
list[int]、dict[str, int];同版本还引入了字典合并运算符|(PEP 584)和str.removeprefix()/str.removesuffix()。 - 3.10 :引入了「联合类型运算符」
X | Y,以及结构化模式匹配match/case。 - 3.12 :引入了
type语句来声明类型别名,以及类型参数语法,函数和类可以直接写def first[T](items: list[T]) -> T:(PEP 695)。同时typing.TypeAlias被标记为废弃,官方建议改用type语句,并计划在 3.18 移除。 - 3.14:注解的求值时机发生根本变化,见下一节。
python
# 适用于 Python 3.12+
# type 语句声明类型别名(3.12 起)
type Vector = list[float]
# 类型参数语法(3.12 起,PEP 695)
def first[T](items: list[T]) -> T:
return items[0]
v: Vector = [1.0, 2.0]
print(first([10, 20, 30])) # 10
print(first(v)) # 1.0
在 3.12 之前要表达同样的意思,得这样写:
python
# 适用于 Python 3.8+
from typing import TypeVar, List
T = TypeVar("T")
def first(items: List[T]) -> T:
return items[0]
Vector = List[float]
print(first([10, 20, 30]))
四、PEP 649 / 749:3.14 的延迟求值
这是本节标题里「最新」二字真正的落点。
在 3.14 之前,函数、类、模块上的注解会在定义那一刻 立即求值。这带来一个麻烦:如果注解里引用了还没定义的名字(比如类里引用自身),就会在定义时抛 NameError。过去的对策是把注解写成字符串,或者加一句 from __future__ import annotations(PEP 563),让所有注解都变成字符串存着、永不求值。
Python 3.14 起,注解不再在定义时立即求值 ,而是延迟到真正需要读取时才求值(PEP 649,配套的落地实现是 PEP 749)。好处有两方面:定义大量带注解的函数和类时,少了立即求值的开销;同时不需要再给前向引用套引号,也不必为了这个去开 from __future__ import annotations。
需要读取注解时,标准库提供了 annotationlib 模块,可以指定按哪种格式取:
| 格式 | 含义 |
|---|
|----------------|----------------------------|
| Format.VALUE | 求值成真实的运行时对象,最接近 3.14 之前的行为 |
|---------------------|--------------------------|
| Format.FORWARDREF | 尽量求值,求不出来的部分用前向引用占位,不抛异常 |
|-----------------|------------------------------------------------------|
| Format.STRING | 返回字符串形式,等价于 from __future__ import annotations 的效果 |
python
# 适用于 Python 3.14+
from annotationlib import get_annotations, Format
def f(x: Undefined, y: "int") -> None: # Undefined 从未定义,也不报错
pass
print(get_annotations(f, format=Format.STRING)) # 全部以字符串形式给出
print(get_annotations(f, format=Format.FORWARDREF)) # 能求的求真,不能的占位
上面这段在 3.14 之前会在 def 那一行就抛 NameError。如果运行环境低于 3.14,要在定义前把 Undefined 定义出来,或改用字符串注解、加 from __future__ import annotations。
再看前向引用:3.14 之前,类里引用自身的方法返回值常常得写成字符串。
python
# 适用于 Python 3.8+
class Node:
def __init__(self, value: int, nxt: "Node" = None) -> None:
self.value = value
self.nxt = nxt
def __repr__(self) -> str:
return f"Node({self.value})"
print(Node(1, Node(2)))
在 3.14 及以上,"Node" 的引号可以去掉,直接写 nxt: Node = None 也不会报错。但为了兼容更老的版本,保留引号仍然是最保险的写法------引号在任何 3.x 下都合法。
常见坑点
- 以为注解会在运行时拦截错误类型。
❌ 依赖 def f(x: int) -> int 来保证调用方一定传整数。
✅ 注解只给静态检查器和 IDE 用;要运行时校验必须自己写判断,或使用会读注解并校验的第三方库。
- 在 3.9 之前的版本写
list[int]。
❌ 在 3.8 上执行 def f(x: list[int]) -> None:,运行到这一行会抛 TypeError。
✅ 3.9 以下用 from typing import List 写 List[int],或加 from __future__ import annotations 让注解不求值。
- 把
Optional[X]当成「可以省略这个参数」。
❌ 写 def f(x: Optional[int]) 却希望调用方能不传 x。
✅ Optional[int] 只表示「值可能是 None」;「可以不传」必须靠默认值 x: int | None = None。
- 注解里引用了当前作用域没有的名字。
❌ def f(x: Undefined) -> None: 在 3.14 之前会在定义时就抛 NameError。
✅ 3.14 起延迟求值,不会立刻报错;更早的版本加 from __future__ import annotations,或用字符串注解。
- 用了已废弃的类型别名写法。
❌ 3.12 之后继续用 typing.TypeAlias / typing.List / typing.Dict 写新代码。
✅ 类型别名改用 type X = ... 语句;容器泛型改用内置的 list[...]、dict[...]。
- 把
Any当成万能安全阀。
❌ 给所有拿不准的参数都标 Any,等于把类型检查关掉。
✅ 尽量给出具体的联合类型;Any 只在确实无法表达时局部使用。
- 以为
__annotations__取到的永远是运行时对象。
❌ 写了 from __future__ import annotations 后,还指望 f.__annotations__ 里的值是真实类型------它存的是字符串。
✅ 需要真实类型时用 typing.get_type_hints(f) 求值,或在 3.14 及以上用 annotationlib 指定格式。
- 类型注解写对了就以为代码一定对。
❌ 用类型检查器零报错作为「逻辑正确」的证明。
✅ 类型检查只覆盖类型层面的错误,逻辑错误、边界条件、并发问题它一概不管。
总结
| 特性 | 引入版本 | 说明 |
|---|
|------|------------------------|--------------|
| 类型注解 | 3.0(函数注解)/ 3.5(typing) | 元数据,不参与运行时校验 |
|--------------------------------------|--------------|-------------|
| from __future__ import annotations | 3.7(PEP 563) | 让注解以字符串形式保存 |
|------------------|-----|------------------|
| 内置泛型 list[int] | 3.9 | 替代 typing.List |
|-------------|-----|------|
| 联合写法 `X \ | Y` | 3.10 |
|------------------|------|----------------|
| type 语句、类型参数语法 | 3.12 | 替代 TypeAlias |
|------------|---------------------|------------------------------|
| 注解延迟求值默认开启 | 3.14(PEP 649 / 749) | 定义时不再求值,配 annotationlib 读取 |
用新代码时记住三条:容器泛型写内置形式、联合类型写 |、别再从 typing 里搬那些已被内置类型取代的别名。3.14 的延迟求值让注解「什么时候算出来」这件事变得可控,但「注解不参与运行时校验」这条铁律,从 3.0 到现在都没变过。