3.11 Python

Release - 3.11.4

Date - 23.06.26

1.1 Release Highlights

1.1.1 更快的速度

Python 3.11 比 Python 3.10 快 10-60%。平均而言,我们在标准基准测试套件上测得了 1.25 倍的加速。

1.

1.2 New Features

1.2.1 问题回溯

打印回溯时,解释器现在将指向导致错误的确切表达式,而不仅仅是行

image-20230627154048198

image-20230627154104249

1.2.2 Exception Group/except*

1) Exception Group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>>> eg = ExceptionGroup(
... "one",
... [
... TypeError(1),
... ExceptionGroup(
... "two",
... [TypeError(2), ValueError(3)]
... ),
... ExceptionGroup(
... "three",
... [OSError(4)]
... )
... ]
... )
>>> import traceback
>>> traceback.print_exception(eg)
| ExceptionGroup: one (3 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 1
+---------------- 2 ----------------
| ExceptionGroup: two (2 sub-exceptions)
+-+---------------- 1 ----------------
| TypeError: 2
+---------------- 2 ----------------
| ValueError: 3
+------------------------------------
+---------------- 3 ----------------
| ExceptionGroup: three (1 sub-exception)
+-+---------------- 1 ----------------
| OSError: 4

2) except*

* 符号表示每个 except* 子句可以处理多个异常

  • 在传统的 try-except 语句中,只有一个异常需要处理,因此最多执行一个 except 子句的主体;第一个与异常匹配的
  • 使用新语法, except* 子句可以匹配引发的异常组的子组,而其余部分由以下 except* 个子句匹配。换句话说,单个异常组可能导致执行多个 except* 个子句,但每个此类子句最多执行一次(对于组中所有匹配的异常),并且每个异常要么只由一个子句(与其类型匹配的第一个子句)处理,要么在最后重新引发
1
2
3
4
5
6
7
8
try:
...
except* SpamError:
...
except* FooError as e:
...
except* (BarError, BazError) as e:
...

1.2.3 可变参数泛型

允许使用新引入的任意长度类型变量 TypeVarTuple 定义通用的 Array 类其形状(和数据类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# class
from typing import TypeVar, TypeVarTuple

DType = TypeVar('DType')
Shape = TypeVarTuple('Shape')

class Array(Generic[DType, *Shape]): # 必须 unpack 解压缩

def __abs__(self) -> Array[DType, *Shape]: ...

def __add__(self, other: Array[DType, *Shape]) -> Array[DType, *Shape]: ...

# other1
from typing import NewType

Height = NewType('Height', int)
Width = NewType('Width', int)

x: Array[float, Height, Width] = Array()

# other2
from typing import Literal as L

x: Array[float, L[480], L[640]] = Array()

1.2.4 TypedDict 非必须

RequiredNotRequired 提供了一种直接的方法来标记 TypedDict 中的各个项目是否必须存在。(以前,这只能使用继承来实现)

默认情况下,所有字段仍然是必需的,除非 total 参数设置为 False ,在这种情况下,默认情况下仍然不需要所有字段。例如,以下内容指定一个 TypedDict ,其中包含一个必需键和一个非必需键:

1
2
3
4
5
6
7
8
9
10
11
12
class Movie(TypedDict):
title: str
year: NotRequired[int]

m1: Movie = {"title": "Black Panther", "year": 2018} # OK
m2: Movie = {"title": "Star Wars"} # OK (year is not required)
m3: Movie = {"year": 2022} # ERROR (missing required field title)

# 等效于
class Movie(TypedDict, total=False):
title: Required[str]
year: int

1.2.5 Self type

新的 Self 批注提供了一种简单直观的方法来批注返回其类实例的方法

常见用例包括以 classmethod 秒形式提供的替代构造函数,以及返回 self__enter__() 方法:

1
2
3
4
5
6
7
8
9
10
11
class MyLock:
def __enter__(self) -> Self:
self.lock()
return self
...

class MyInt:
@classmethod
def fromhex(cls, s: str) -> Self:
return cls(int(s, 16))
...

1.2.6 LiteralString

新的 LiteralString 批注可用于指示函数参数可以是任何文本字符串类型。这允许函数接受任意文本字符串类型,以及从其他文本字符串创建的字符串。然后,类型检查器可以强制敏感函数(例如执行 SQL 语句或 shell 命令的函数)仅使用静态参数调用,从而提供针对注入攻击的保护。

例如,可以按如下方式批注 SQL 查询函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def run_query(sql: LiteralString) -> ...
...

def caller(
arbitrary_string: str,
query_string: LiteralString,
table_name: LiteralString,
) -> None:
run_query("SELECT * FROM students") # ok
run_query(query_string) # ok
run_query("SELECT * FROM " + table_name) # ok
run_query(arbitrary_string) # type checker error
run_query( # type checker error
f"SELECT * FROM students WHERE name = {arbitrary_string}"
)

1.2.7 数据类转换

dataclass_transform 可用于修饰类、元类或本身就是装饰器的函数。 @dataclass_transform() 的存在告诉静态类型检查器,修饰的对象执行运行时“魔术”来转换类,为其提供类似 dataclass 的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# The create_model decorator is defined by a library.
@typing.dataclass_transform()
def create_model(cls: Type[T]) -> Type[T]:
cls.__init__ = ...
cls.__eq__ = ...
cls.__ne__ = ...
return cls

# The create_model decorator can now be used to create new model classes:
@create_model
class CustomerModel:
id: int
name: str

c = CustomerModel(id=327, name="Eric Idle")

1.3 Other Language Changes

带星标的解包表达式现在可以在 for 语句中使用

1
2
for x in *a, *b:
print(x)

3.10 Python

Release - 3.10

1.1 New Features

1.1.1 圆括号上下文管理器

现在已支持使用外层圆括号来使多个上下文管理器可以连续多行地书写。 这允许将过长的上下文管理器集能够以与之前 import 语句类似的方式格式化为多行的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
with (CtxManager() as example):
...

with (
CtxManager1(),
CtxManager2()
):
...

with (CtxManager1() as example,
CtxManager2()):
...

with (CtxManager1(),
CtxManager2() as example):
...

# 在被包含的分组末尾过可以使用一个逗号作为结束
with (
CtxManager1() as example1,
CtxManager2() as example2
):
...

1.1.2 错误消息

1)SyntaxError - 语法错误

现在当解析包含有未关闭括号的代码时解释器会包括未关闭括号的位置而不是显示 SyntaxError: unexpected EOF while parsing 并指向某个不正确的位置

1
2
3
4
5
6
7
8
9
expected = {9: 1, 18: 2, 19: 2, 27: 3, 28: 3, 29: 3, 36: 4, 37: 4,
38: 4, 39: 4, 45: 5, 46: 5, 47: 5, 48: 5, 49: 5, 54: 6,
some_other_code = foo()

# SyntaxError: invalid syntax
File "example.py", line 1
expected = {9: 1, 18: 2, 19: 2, 27: 3, 28: 3, 29: 3, 36: 4, 37: 4,
^
SyntaxError: '{' was never closed

2)IndentationError - 缩进错误

许多 IndentationError 异常现在具有更多上下文来提示是何种代码块需要缩进,包括语句的位置:

1
2
3
4
5
6
7
8
>>> def foo():
... if lel:
... x = 2

File "<stdin>", line 3
x = 2
^
IndentationError: expected an indented block after 'if' statement in line 2

3)AttributeError - 属性错误

当打印 AttributeError 时,PyErr_Display() 将提供引发异常的对象中类似属性名称的建议

1
2
3
4
5
>>> collections.namedtoplo

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'collections' has no attribute 'namedtoplo'. Did you mean: namedtuple?

4)NameError - 名称错误

当打印解释器所引发的 NameError 时,PyErr_Display() 将提供引发异常的函数中类似变量名称的建议

1
2
3
4
5
6
>>> schwarzschild_black_hole = None
>>> schwarschild_black_hole

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'schwarschild_black_hole' is not defined. Did you mean: schwarzschild_black_hole?

1.1.3 结构化模式匹配 match

增加了采用模式加上相应动作的 match 语句case 语句 的形式的结构化模式匹配。 模式由序列、映射、基本数据类型以及类实例构成。 模式匹配使得程序能够从复杂的数据类型中提取信息、根据数据结构实现分支,并基于不同的数据形式应用特定的动作。

1)语法和操作

1
2
3
4
5
6
7
8
9
match subject:
case <pattern_1>:
<action_1>
case <pattern_2>:
<action_2>
case <pattern_3>:
<action_3>
case _:
<action_wildcard>
  • match 语句接受一个表达式并将其值与以一个或多个 case 语句块形式给出的一系列模式进行比较。 具体来说,模式匹配的操作如下:

    1. 使用具有特定类型和形状的数据 (subject)
    2. 针对 subjectmatch 语句中求值
    3. 从上到下对 subject 与 case 语句中的每个模式进行比较直到确认匹配到一个模式。
    4. 执行与被确认匹配的模式相关联的动作。
    5. 如果没有确认到一个完全的匹配,则如果提供了使用通配符 _ 的最后一个 case 语句,则它将被用作已匹配模式。 如果没有确认到一个完全的匹配并且不存在使用通配符的 case 语句,则整个 match 代码块不执行任何操作。

2)声明性方式

虽然使用嵌套的“if”语句的“命令性”系列指令可以被用来完成类似结构化模式匹配的效果,但它没有“声明性”方式那样清晰。 相反地,“声明性”方式指定了一个匹配所要满足的条件,并且通过其显式的模式使之更为易读。

虽然结构化模式匹配可以采取将一个变量与一个 case 语句中的字面值进行比较的最简单形式来使用,但它对于 Python 的真正价值在于其针对目标类型和形状的处理操作。

3)简单模式

1
2
3
4
5
6
7
8
9
10
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"
  1. 你可以使用 | (“ or ”)在一个模式中组合几个字面值:

    1
    2
    case 401 | 403 | 404:
    return "Not allowed"
  2. 无通配符的行为:不在 case 语句中使用_

4)带有字面值和变量的模式

模式可以看起来像解包形式,而且模式可以用来绑定变量。在这个例子中,一个数据点可以被解包为它的 x 坐标和 y 坐标:

1
2
3
4
5
6
7
8
9
10
11
12
# point is an (x, y) tuple
match point:
case (0, 0):
print("Origin")
case (0, y):
print(f"Y={y}")
case (x, 0):
print(f"X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("Not a point")

5)模式和类

如果你使用类来结构化你的数据,你可以使用类的名字,后面跟一个类似构造函数的参数列表,作为一种模式。这种模式可以将类的属性捕捉到变量中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point:
x: int
y: int

def location(point):
match point:
case Point(x=0, y=0):
print("Origin is the point's location.")
case Point(x=0, y=y):
print(f"Y={y} and the point is on the y-axis.")
case Point(x=x, y=0):
print(f"X={x} and the point is on the x-axis.")
case Point():
print("The point is located somewhere else on the plane.")
case _:
print("Not a point")
  • 带有位置参数的模式

    • 你可以在某些为其属性提供了排序的内置类(例如 dataclass)中使用位置参数。 你也可以通过在你的类中设置 __match_args__ 特殊属性来为模式中的属性定义一个专门的位置。 如果它被设为 (“x”, “y”),则以下模式均为等价的(并且都是将 y 属性绑定到 var 变量):

      1
      2
      3
      4
      Point(1, var)
      Point(1, y=var)
      Point(x=1, y=var)
      Point(y=var, x=1)

6)嵌套模式

模式可以任意地嵌套。 例如,如果我们的数据是由点组成的短列表,则它可以这样被匹配:

1
2
3
4
5
6
7
8
9
10
11
match points:
case []:
print("No points in the list.")
case [Point(0, 0)]:
print("The origin is the only point in the list.")
case [Point(x, y)]:
print(f"A single point {x}, {y} is in the list.")
case [Point(0, y1), Point(0, y2)]:
print(f"Two points on the Y axis at {y1}, {y2} are in the list.")
case _:
print("Something else is found in the list.")

7)复杂模式和通配符

到目前为止,这些例子仅在最后一个 case 语句中使用了 _。 但通配符可以被用在更复杂的模式中,例如 ('error', code, _)。 举例来说:

1
2
3
4
5
match test_variable:
case ('warning', code, 40):
print("A warning has been received.")
case ('error', code, _):
print(f"An error {code} occurred.")

在上述情况下,test_variable 将可匹配 (‘error’, code, 100) 和 (‘error’, code, 800)。

8)约束项

我们可以向一个模式==添加 if 子句==,称为“约束项”。 如果约束项为假值,则 match 将继续尝试下一个 case 语句块。 请注意值的捕获发生在约束项被求值之前:

1
2
3
4
5
match point:
case Point(x, y) if x == y:
print(f"The point is located on the diagonal Y=X at {x}.")
case Point(x, y):
print(f"Point is not on the diagonal.")

9)其他关键特性

  • 类似于解包赋值,元组和列表模式具有完全相同的含义,而且实际上能匹配任意序列。 从技术上说,目标必须为一个序列。 因而,一个重要的例外是模式不能匹配迭代器。 而且,为了避免一个常见的错误,序列模式不能匹配字符串。

  • 序列模式支持通配符: [x, y, *rest](x, y, *rest) 的作用类似于解包赋值中的通配符。 在 * 之后的名称也可以为 _,因此 (x, y, *_) 可以匹配包含两个条目的序列而不必绑定其余的条目。

  • 映射模式: {"bandwidth": b, "latency": l} 会从一个字典中捕获 "bandwidth""latency" 值。 与序列模式不同,额外的键会被忽略。 也支持通配符 **rest。 (但 **_ 是冗余的,因而不被允许。)

  • ==子模式可使用 as 关键字来捕获:==

    1
    case (Point(x1, y1), Point(x2, y2) as p2): ...

    x1, y1, x2, y2 等绑定就如你在没有 as 子句的情况下所期望的,而 p2 会绑定目标的整个第二项。

  • 大多数字面值是按相等性比较的。 但是,==单例对象== True, FalseNone 则是按标识号比较的。

  • ==命名常量==也可以在模式中使用。 这些命名常量必须为带点号的名称以防止常量被解读为捕获变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from enum import Enum
    class Color(Enum):
    RED = 0
    GREEN = 1
    BLUE = 2

    match color:
    case Color.RED:
    print("I see red!")
    case Color.GREEN:
    print("Grass is green")
    case Color.BLUE:
    print("I'm feeling the blues :(")

1.1.4 类型提示

1)类型联合运算符

引入了启用 X | Y 语法的类型联合运算符

在之前的 Python 版本中,要为可接受多种类型参数的函数应用类型提示,使用的是 typing.Union

1
2
def square(number: Union[int, float]) -> Union[int, float]:
return number ** 2

类型提示现在可以使用更简洁的写法:

1
2
def square(number: int | float) -> int | float:
return number ** 2

这个新增语法也被接受作为 isinstance()issubclass()的第二个参数:

1
2
>>> isinstance(1, int | str)
True

2)类型别名

在 Python 的早期版本中,增加了类型别名,以允许我们创建表示用户定义类型的别名。在 Python 3.9 或更早的版本中,可以这样写:

1
2
3
4
FileName = str

def parse(file: FileName) -> None:
...

这里 FileName 是基本 Python 字符串类型的别名。不过,从 Python 3.10 开始,定义类型别名的语法将改为:

1
2
3
4
FileName: TypeAlias = str

def parse(file: FileName) -> None:
...

1.2 Other Language Changes

1.2.1 int

int 类型新增了一个方法 int.bit_count(),返回给定整数的二进制展开中值为一的位数,或称“比特计量”。

1.2.2 dict

现在 dict.keys(), dict.values()dict.items() 所返回的视图都有一个 mapping 属性,它给出包装了原始字典的 types.MappingProxyType 对象。

1.2.3 zip()

现在 zip() 函数有一个可选的 strict 旗标,用于要求所有可迭代对象的长度都相等。

3.9 Python

Release - 3.9

1.1 New Features

1.1.1 字典合并和更新运算符

合并 (|) 与更新 (|=) 运算符已被加入内置的 dict 类。 它们为现有的 dict.update{**d1, **d2} 字典合并方法提供了补充。

1
2
3
4
5
6
>>> x = {"key1": "value1 from x", "key2": "value2 from x"}
>>> y = {"key2": "value2 from y", "key3": "value3 from y"}
>>> x | y
{'key1': 'value1 from x', 'key2': 'value2 from y', 'key3': 'value3 from y'}
>>> y | x
{'key2': 'value2 from x', 'key3': 'value3 from y', 'key1': 'value1 from x'}

1.1.2 移除前缀和后缀字符串方法

增加了 str.removeprefix(prefix)str.removesuffix(suffix) 用于方便地从字符串移除不需要的前缀或后缀。 也增加了 bytes,bytearray 以及 collections.UserString 的对应方法。

1.1.3 多项集中的类型标注泛型

在类型标注中现在你可以使用内置多项集类型例如 listdict 作为通用类型而不必从 typing 导入对应的大写形式类型名 (例如 ListDict)。 标准库中的其他一些类型现在同样也是通用的,例如 queue.Queue

1
2
3
def greet_all(names: list[str]) -> None:
for name in names:
print("Hello", name)

1.2 Other Language Changes

  • __import__() 现在会引发 ImportError 而不是 ValueError,后者曾经会在相对导入超出其最高层级包时发生。

  • Python 现在会获取命令行中指定的脚本文件名 (例如: python3 script.py) 的绝对路径: __main__模块的==__file__属性将是一个绝对路径==,而不是相对路径。 现在此路径在当前目录通过 os.chdir() 被改变后仍将保持有效。 作为附带效果,回溯信息也将在此情况下为__main__模块帧显示绝对路径。

  • "".replace("", s, n) 对于所有非零的 n 都将返回 s 而不是空字符串。 现在此方法会与 "".replace("", s) 保持一致。 对于 bytesbytearray 对象也有类似的修改。

  • 未加圆括号的 lambda 表达式不能再作为推导式和生成器表达式的 if 子句的表达式部分。

3.8 Python

Release - 3.8

1.1 New Features

1.1.1 赋值表达式 :=

新增的语法 := 可在表达式内部为变量赋值。 它被昵称为“海象运算符”因为它很像是 海象的眼睛和长牙

在这个示例中,赋值表达式可以避免调用 len() 两次:

1
2
if (n := len(a)) > 10:
print(f"List is too long ({n} elements, expected <= 10)")

类似的益处还可出现在正则表达式匹配中需要使用两次匹配对象的情况中,一次检测用于匹配是否发生,另一次用于提取子分组:

1
2
3
discount = 0.0
if (mo := re.search(r'(\d+)% discount', advertisement)):
discount = float(mo.group(1)) / 100.0

此运算符也适用于配合 while 循环计算一个值来检测循环是否终止,而同一个值又在循环体中再次被使用的情况:

1
2
3
# Loop over fixed length blocks
while (block := f.read(256)) != '':
process(block)

另一个值得介绍的用例出现于列表推导式中,在筛选条件中计算一个值,而同一个值又在表达式中需要被使用:

1
2
[clean_name.title() for name in names
if (clean_name := normalize('NFC', name)) in allowed_names]

请尽量将海象运算符的使用限制在清晰的场合中,以降低复杂性并提升可读性。

1.1.2 仅限位置形参

有一个新的函数参数语法 / ,指示某些函数参数必须按位置指定,不能用作关键字参数。

1)基本用法

在下面的例子中,形参 ab 为仅限位置形参,cd 可以是位置形参或关键字形参,而 ef 要求为关键字形参:

1
2
def f(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)

以下均为合法的调用:

1
f(10, 20, 30, d=40, e=50, f=60)

但是,以下均为不合法的调用:

1
2
f(10, b=20, c=30, d=40, e=50, f=60)   # b cannot be a keyword argument
f(10, 20, 30, 40, 50, f=60) # e must be a keyword argument

2)作用

  • 不接受关键字参数

    1
    2
    3
    def divmod(a, b, /):
    "模拟内置 divmod() 函数"
    return (a // b, a % b)
  • 不需要形参名称时排出关键字参数

    1
    len(obj='hello')  # “obj”关键字参数会损害可读性
  • 将形参标记为仅限位置形参将允许在未来修改形参名而不会破坏客户的代码

    1
    2
    3
    # 形参名 dist 在未来可能被修改
    def quantiles(dist, /, *, n=4, method='exclusive')
    ...

3)其他参数

由于在 / 左侧的形参不会被公开为可用关键字,其他形参名仍可在 **kwargs 中使用:

1
2
3
4
5
>>> def f(a, b, /, **kwargs):
... print(a, b, kwargs)
...
>>> f(10, 20, a=1, b=2, c=3) # a and b are used in two ways
10 20 {'a': 1, 'b': 2, 'c': 3}

1.1.3 f 字符串 =

增加 = 说明符用于 f-string。 形式为 f'{expr=}'的 f-字符串将扩展表示为表达式文本,加一个等于号,再加表达式的求值结果。

换符 '!s' 调用 str() 转换求值结果,'!r' 调用 repr()'!a' 调用 ascii()。(默认调用 repr()

例如:

1
2
3
4
>>> user = 'eric_idle'
>>> member_since = date(1975, 7, 31)
>>> f'{user=} {member_since=}'
"user='eric_idle' member_since=datetime.date(1975, 7, 31)"

通常的 f-字符串格式说明符允许更细致地控制所要显示的表达式结果:

1
2
3
>>> delta = date.today() - member_since
>>> f'{user=!s} {delta.days=:,d}'
'user=eric_idle delta.days=16,075'

= 说明符将输出整个表达式,以便详细演示计算过程:

1
2
>>> print(f'{theta=}  {cos(radians(theta))=:.3f}')
theta=30 cos(radians(theta))=0.866