Python 3.12 里几个能立刻用上的新写法

78 阅读6分钟

3.12 有几个改动属于“知道之后代码立刻能短一截”的类型。下面这些都在本机 Python 3.12.10 上跑过,输出是脚本实跑的结果。

泛型不用再 import TypeVar 了

Python312特性总览图 以前写一个泛型栈,开头得先摆三行样板:

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,不是「先别用新语法就行」那么简单。