3.12 有几个改动属于“知道之后代码立刻能短一截”的类型。下面这些都在本机 Python 3.12.10 上跑过,输出是脚本实跑的结果。
泛型不用再 import TypeVar 了
以前写一个泛型栈,开头得先摆三行样板:
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
...
3.12 的 PEP 695 把 TypeVar 变成了语法的一部分,方括号直接写在类名和函数名后面:
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def first[T](items: list[T]) -> T | None:
return items[0] if items else None
类型参数是真实存在的对象,不是装饰器语法糖:
Stack.__type_params__ = (T,)
first.__type_params__ = (T,)
好处是两件事:少一个 import,以及类型参数不再泄漏到模块命名空间里。以前 T = TypeVar("T") 是模块级的一个真实对象,整个文件都看得见它。
PEP 695 的类型参数只活在声明它的类或函数内部,出了这个范围就看不到,同名也不会撞车。声明完之后在模块级打印它就能看到差别:
print(T) # NameError: name 'T' is not defined
旧写法的 T 一直到模块级都能取到,两者不是同一个对象。
type 语句:类型别名终于是个正式语法
以前写类型别名有两种写法,都不太理想:
Vector = list[float] # 就是个普通变量, 表达不了泛型参数
Vector: TypeAlias = list[float] # 得从 typing 导入 TypeAlias
3.12 加了 type 语句:
type Vector = list[float]
type Matrix[T] = list[list[T]]
它和普通赋值的关键区别是:type 语句声明的是「类型别名」这件事本身,而不是一个恰好指向类型的变量。 它支持泛型参数,运行时也是独立的别名对象:
Matrix[int] -> [[1, 2], [3, 4]]
Matrix.__type_params__ = (T,)
读代码时的收益比写代码时更大——看到 type 开头就知道这是别名,不是某个模块级常量。
itertools.batched:分批终于不用自己写
"把列表按 N 个一组切开"这个需求,几乎每个项目里都有一段手写的实现,而且尾巴那组经常处理错。3.12 加了标准实现:
import itertools
data = list(range(7))
print(list(itertools.batched(data, 3)))
[(0, 1, 2), (3, 4, 5), (6,)]
最后一批不足 3 个,照常返回 (6,)。返回的是元组,不是列表。
有个版本细节要提醒:strict=True 参数是 3.13 才加的,3.12 用了会直接报错:
TypeError: batched() takes at most 2 arguments (3 given)
网上有些文章把 3.13 的用法和 3.12 混在一起写,照着抄会在这里卡住。
f-string 的三条限制解除了
PEP 701 把 f-string 的解析器重写了。下面这三件事在 3.11 上都是语法错误, 3.12 才能跑。三条报错都在本机 3.11.7 上实跑过:
name = "XianLin"
# 1. 表达式里用与外层相同的引号类型
# 3.11 报 SyntaxError: f-string: expecting '}'
print(f"同类引号: {"paid" if True else "x"} / {name}")
# 2. 表达式里出现反斜杠。内外引号必须不同,
# 否则先撞上第 1 条,看不到这条限制自己的报错
# 3.11 报 SyntaxError: f-string expression part cannot include a backslash
print(f"换行符: {'a\nb'.splitlines()}")
# 3. 表达式跨多行
# 3.11 报 SyntaxError: unterminated string literal (detected at line 1)
print(f"多行表达式: {sum(
i * 2
for i in range(4)
)}")
本机 3.12.10 的实际输出:
同类引号: paid / XianLin
换行符: ['a', 'b']
多行表达式: 12
这三行逐字复制自 verify_312_features.py 的完整输出,证据文件里可对账。
3.11 上写 f-string 里的字典取值,外层用双引号时内层必须换成单引号,嵌套两层就得数引号:
# 3.11 得这么写(内外引号必须不同)
print(f"{data['key']}")
3.12 之后内外可以是同一种引号,嵌套多层也不用换。
不过第三条有个反面用法值得提:多行表达式虽然合法,但别在 f-string 里塞复杂逻辑。
上面的 sum(...) 用来演示可以,真实代码里抽成变量更好读。
typing.override:拼错方法名能被查出来
子类覆盖父类方法时,方法名打错是很常见的手误,运行时完全看不出来——你以为覆盖了,实际上父类的方法还在生效。
from typing import override
class Base:
def run(self) -> str:
return "base"
class Child(Base):
@override
def run(self) -> str:
return "child"
@override 在运行时只给函数打一个 __override__ = True 的标记,不包装饰器、不改调用路径。它的价值在类型检查阶段:父类里没有同名方法时(比如你把 run 打成了 runn),mypy 和 pyright 会据这个标记报错。收益全在静态检查,调用开销不变。
列表推导式比 append 循环快,但别过度解读
3.12 的 PEP 709 把列表推导式内联了,不再为每次推导创建独立的函数帧。实测对比:
推导式 0.156~0.173s vs append 循环 0.185~0.197s (推导式 1.14~1.18x)
上面给的是区间而不是单个值,三次跑的是同一段代码,单次值在证据文件里。
三次方向一致,都是推导式更快。计时受机器负载影响,单次值不该当结论,这就是给区间的原因。
推导式本来就比手写 append 快,所以这个数字不能直接当成"3.12 比 3.11 快 14%~18%"。
要用它判断版本收益,得在两个 Python 版本上跑同一段代码才有意义。这里放出来只是说明一件事:推导式不是语法糖,它是有性能理由的写法。
报错信息给的建议更全了
这一节要分清版本,因为两个建议不是同一批加的(本机 3.11.7 与 3.12.10 对跑核实):
- 属性名建议早于 3.12,本机 3.11.7 上就有
- 导入名建议(
from os import pth提示Did you mean: 'path'?)才是 3.12 新增的
3.12 的改动是把建议扩展到 ImportError 这一类,并让消息组织得更清楚。 所以严格说,这一节讲的是「3.12 补齐了导入名建议」,不是「3.12 开始给建议」。 两类拼写错误现在都有候选:
import itertools
itertools.batchd # AttributeError
AttributeError: module 'itertools' has no attribute 'batchd'. Did you mean: 'batched'?
同一条语句在 3.11.7 上没有候选:3.11 的 itertools 里压根没有 batched 这个名字,猜也没得猜。
from os import pth # ImportError
ImportError: cannot import name 'pth' from 'os' (<安装路径>\Lib\os.py). Did you mean: 'path'?
尖括号处的安装路径已省略。同一句在 3.11.7 上照样报错,只是没有后半句建议。
注意这里有个容易搞错的细节:ModuleNotFoundError 不会给建议。 实测 import itertool,输出仍然是干巴巴的一行:
ModuleNotFoundError: No module named 'itertool'
规律是模块内部的名字猜错了会提示,整个模块找不到时不提示——毕竟模块可能压根没装,猜也没意义。
一个提醒
上面这些改动里,class Stack[T] 和 type Vector = ... 都是语法级改动。
语法一旦用了,代码就再也跑不回 3.11 及以下——不是 import 不到东西那种能靠 try/except 兼容的情况,而是直接 SyntaxError。
所以用之前先确认两件事:运行时的 Python 版本,以及 CI 里用的是哪个版本。有库作者在本地用 3.12 写得顺手,推上去之后 CI 跑 3.10 直接挂掉。
现在可以跑一下这两条,确认自己的环境:
python -c "import sys; print(sys.version)"
python -c "from typing import override; print('3.12+')"
第一条低于 3.12 的话,上面这些内容全都先别往项目里写——包括 itertools.batched。它是 3.12 才加进标准库的,3.11 上直接 AttributeError,不是「先别用新语法就行」那么简单。