Python 3.11中有什么新内容?

331 阅读9分钟

Python 3.11的第一个测试版已经发布,带来了一些迷人的功能供我们修补。这是今年晚些时候在2022年发布的Python中你可以期待看到的东西。

甚至更好的错误信息

Python 3.10在各方面给了我们更好的错误信息,但Python 3.11的目标是更加完善它们。在Python 3.11中被添加到错误信息中的一些最重要的东西是。

回溯中的确切错误位置

到目前为止,在跟踪回溯中,你得到的关于异常发生地点的唯一信息是行。但问题可能发生在行的任何地方,所以有时这些信息是不够的。

这里有一个例子。

def get_margin(data):
    margin = data['profits']['monthly'] / 10 + data['profits']['yearly'] / 2
    return margin

data = {
    'profits': {
        'monthly': 0.82,
        'yearly': None,
    },
    'losses': {
        'monthly': 0.23,
        'yearly': 1.38,
    },
}
print(get_margin(data))

这段代码导致了一个错误,因为字典中的一个字段是None 。这就是我们得到的结果。

Traceback (most recent call last):
  File "/Users/tusharsadhwani/code/marvin-python/mytest.py", line 15, in <module>
    print(get_margin(data))
  File "/Users/tusharsadhwani/code/marvin-python/mytest.py", line 2, in print_margin
    margin = data['profits']['monthly'] / 10 + data['profits']['yearly'] / 2
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'

但通过回溯本身,我们不可能知道是哪一部分的计算导致了这个错误。

然而在3.11中。

Traceback (most recent call last):
  File "asd.py", line 15, in <module>
    print(get_margin(data))
          ^^^^^^^^^^^^^^^^
  File "asd.py", line 2, in print_margin
    margin = data['profits']['monthly'] / 10 + data['profits']['yearly'] / 2
                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~
TypeError: unsupported operand type(s) for /: 'NoneType' and 'int'

很明显,data['profits']['yearly']None

为了能够呈现这些信息,end_lineend_col 数据被添加到 Python 代码对象中。你也可以通过obj.__code__.co_positions() 方法直接访问这些信息。

异常情况的说明

为了使回溯的内容更加丰富,Python 3.11 允许你向异常对象添加注释,这些注释会被存储在异常中,并在异常发生时显示出来。

以这段代码为例,我们在其中添加了关于一些 API 数据转换逻辑的重要信息。

def get_seconds(data):
    try:
        milliseconds = float(data['milliseconds'])
    except ValueError as exc:
        exc.add_note(
            "The time field should always be a number, this is a critial bug. "
            "Please report this to the backend team immediately."
        )
        raise  # re-raises the exception, instead of silencing it

    seconds = milliseconds / 1000
    return seconds

get_seconds({'milliseconds': 'foo'})  # 'foo' is not a number!

这个添加的注释会被打印在异常信息的下面。

Traceback (most recent call last):
  File "asd.py", line 14, in <module>
    get_seconds({"milliseconds": "foo"})
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "asd.py", line 3, in get_seconds
    milliseconds = float(data["milliseconds"])
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: 'foo'
The time field should always be a number, this is a critial bug. Please report this to the backend team immediately.

内置的toml 支持

标准库现在内置了对阅读TOML文件的支持,使用tomllib 模块。

import tomllib

with open('.deepsource.toml', 'rb') as file:
    data = tomllib.load(file)

tomllib 实际上是基于一个开源的TOML解析库,叫做 。而目前,只支持读取TOML文件。如果你需要写数据到TOML文件,而不是写TOML文件,可以考虑使用 包。tomli tomli-w

asyncio 任务组

在进行异步编程时,你经常会遇到这样的情况:你必须触发许多任务来同时运行,然后在它们完成后采取一些行动。例如,并行下载一堆图片,然后在最后将它们捆绑成一个压缩文件。

要做到这一点,你需要收集任务,并把它们传递给asyncio.gather.这里有一个用gather 函数并行运行任务的简单例子。

import asyncio

async def simulate_flight(city, departure_time, duration):
    await asyncio.sleep(departure_time)
    print(f"Flight for {city} departing at {departure_time}PM")

    await asyncio.sleep(duration)
    print(f"Flight for {city} arrived.")


flight_schedule = {
    'boston': [3, 2],
    'detroit': [7, 4],
    'new york': [1, 9],
}

async def main():
    tasks = []
    for city, (departure_time, duration) in flight_schedule.items():
        tasks.append(simulate_flight(city, departure_time, duration))

    await asyncio.gather(*tasks)
    print("Simulations done.")

asyncio.run(main())

但要自己维护一个任务列表,以便能够等待它们,这有点笨拙。所以现在在asyncio 中加入了一个新的API,叫做任务组。

import asyncio

async def simulate_flight(city, departure_time, duration):
    await asyncio.sleep(departure_time)
    print(f"Flight for {city} departing at {departure_time}PM")

    await asyncio.sleep(duration)
    print(f"Flight for {city} arrived.")


flight_schedule = {
    'boston': [3, 2],
    'detroit': [7, 4],
    'new york': [1, 9],
}

async def main():
    async with asyncio.TaskGroup() as tg:
        for city, (departure_time, duration) in flight_schedule.items():
            tg.create_task(simulate_flight(city, departure_time, duration))

    print("Simulations done.")

asyncio.run(main())

asyncio.TaskGroup() 上下文管理器退出时,它确保所有在其内部创建的任务都已完成运行。

奖励:异常组

一个类似的功能也被添加到异步任务内部的异常处理中,称为异常组。

假设你有许多异步任务在一起运行,其中一些引发了错误。 目前 Python 的异常处理系统在这种情况下并不能很好地工作。

这里有一个简短的演示,展示了3个并发的崩溃任务的情况。

import asyncio

def bad_task():
    raise ValueError("oops")

async def main():
    tasks = []
    for _ in range(3):
        tasks.append(asyncio.create_task(bad_task()))

    await asyncio.gather(*tasks)

asyncio.run(main())

当你运行这段代码时。

$ python asd.py
Traceback (most recent call last):
  File "asd.py", line 13, in <module>
    asyncio.run(main())
  File "/usr/bin/python3.8/lib/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/bin/python3.8/lib/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "asd.py", line 9, in main
    tasks.append(asyncio.create_task(bad_task()))
  File "asd.py", line 4, in bad_task
    raise ValueError("oops")
ValueError: oops

没有任何迹象表明这些任务中有3个是一起运行的。一旦第一个任务失败,它就会使整个程序崩溃。

但是在Python 3.11中,这个行为要好一些。

import asyncio

async def bad_task():
    raise ValueError("oops")

async def main():
    async with asyncio.TaskGroup() as tg:
        for _ in range(3):
            tg.create_task(bad_task())

asyncio.run(main())
$ python asd.py
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |   File "/usr/local/lib/python3.11/asyncio/runners.py", line 181, in run
  |     return runner.run(main)
  |            ^^^^^^^^^^^^^^^^
  |   File "/usr/local/lib/python3.11/asyncio/runners.py", line 115, in run
  |     return self._loop.run_until_complete(task)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/local/lib/python3.11/asyncio/base_events.py", line 650, in run_until_complete
  |     return future.result()
  |            ^^^^^^^^^^^^^^^
  |   File "<stdin>", line 2, in main
  |   File "/usr/local/lib/python3.11/asyncio/taskgroups.py", line 139, in __aexit__
  |     raise me from None
  |     ^^^^^^^^^^^^^^^^^^
  | ExceptionGroup: unhandled errors in a TaskGroup (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 2, in bad_task
    | ValueError: oops
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 2, in bad_task
    | ValueError: oops
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 2, in bad_task
    | ValueError: oops
    +------------------------------------

异常现在告诉我们有3个错误被抛出,在一个被称为ExceptionGroup 的结构中。

对这些异常组的异常处理也很有趣,你可以做except ExceptionGroup ,一次性地捕获所有的异常。

try:
    asyncio.run(main())
except ExceptionGroup as eg:
    print(f"Caught exceptions: {eg}")
$ python asd.py
Caught exceptions: unhandled errors in a TaskGroup (3 sub-exceptions)

或者你可以使用新的except*语法,根据异常类型来捕捉它们。

try:
    asyncio.run(main())
except* ValueError as eg:
    print(f"Caught ValueErrors: {eg}")
$ python asd.py
Caught ValueErrors: unhandled errors in a TaskGroup (3 sub-exceptions)

类型的改进

typing 模块在这个版本中看到了很多有趣的更新。下面是一些最令人兴奋的更新。

变体泛型

在 Python 3.11 中的typing 模块中加入了对变量泛型的支持。

这意味着,现在你可以定义泛型,其中可以包含任意数量的类型。这对于定义多维数据的泛型方法很有用。

比如说。

from typing import Generic
from typing_extensions import TypeVarTuple, Unpack

Shape = TypeVarTuple('Shape')

class Array(Generic[Unpack[Shape]]):
    ...

# holds 1 dimensional data, like a regular list
items: Array[int] = Array()

# holds 3 dimensional data, for example, X axis, Y axis and value
market_prices: Array[int, int, float] = Array()

# This function takes in an `Array` of any shape, and returns the same shape
def double(array: Array[Unpack[Shape]]) -> Array[Unpack[Shape]]:
    ...

# This function takes an N+2 dimensional array and reduces it to an N dimensional one
def get_values(array: Array[int, int, *Shape]) -> Array[*Shape]:
    ...

# For example:
vector_space: Array[int, int, complex] = Array()
reveal_type(get_values(vector_space))  # revealed type is Array[complex]

变量泛型对于定义映射N维数据的函数非常有用。这个功能对于依赖数据科学库(如numpytensorflow )的类型检查代码库有很大的帮助。

新的Generic[*Shape] 语法只在 Python 3.11 中支持。要在Python 3.10及以下版本中使用这个特性,你可以使用typing.Unpack 内建程序来代替:Generic[Unpack[Shape]]

singledispatch 现在支持联合体

functools.singledispatch 是一种在Python中进行函数重载的巧妙方法,基于类型提示。它的工作方式是定义一个通用函数,并用 来装饰它。然后你可以根据函数参数的类型来定义该函数的特殊变体。@singledispatch

from functools import singledispatch


@singledispatch
def half(x):
    """Returns the half of a number"""
    return x / 2

@half.register
def _(x: int):
    """For integers, return an integer"""
    return x // 2

@half.register
def _(x: list):
    """For a list of items, get the first half of it."""
    list_length = len(x)
    return x[: list_length // 2]

                            # Outputs:
print(half(3.6))            # 1.8
print(half(15))             # 7
print(half([1, 2, 3, 4]))   # [1, 2]

通过检查函数参数的类型,singledispatch可以创建通用函数,为函数重载提供一种非面向对象的方法。

但这都是旧闻了。Python 3.11 带来的是,现在,你可以为这些参数传递联合类型。例如,要为所有的数字类型注册一个函数,以前你必须为每个类型分别注册,如float,complexDecimal

@half.register
def _(x: float):
    return x / 2

@half.register
def _(x: complex):
    return x / 2

@half.register
def _(x: decimal.Decimal):
    return x / 2

但现在,你可以在一个联盟中指定所有这些类型。

@half.register
def _(x: float | complex | decimal.Decimal):
    return x / 2

而代码将完全按照预期工作。

Self 类型

以前,如果你必须定义一个返回类本身对象的类方法,为它添加类型是有点奇怪的,它看起来像这样。

from typing import TypeVar

T = TypeVar('T', bound=type)

class Circle:
    def __init__(self, radius: int) -> None:
        self.radius = radius

    @classmethod
    def from_diameter(cls: T, diameter) -> T:
        circle = cls(radius=diameter/2)
        return circle

为了能够说一个方法返回的类型与类本身相同,你必须定义一个TypeVar ,并说该方法返回的类型与当前类本身相同T

但是有了Self 类型,这些都不需要了。

from typing import Self

class Circle:
    def __init__(self, radius: int) -> None:
        self.radius = radius

    @classmethod
    def from_diameter(cls, diameter) -> Self:
        circle = cls(radius=diameter/2)
        return circle

Required[]NotRequired[]

TypedDict 对于在大量使用字典来存储数据的代码库中添加类型信息来说是非常有用的。下面是你如何使用它们。

from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

user : User = {'name': "Alice", 'age': 31}
reveal_type(user['age'])  # revealed type is 'int'

然而,TypedDict有一个限制,即你不能在字典里面有可选的参数,有点像函数定义里面的默认参数。

例如,你可以用NamedTuple 做到这一点。

from typing import NamedTuple

class User(NamedTuple):
    name: str
    age: int
    married: bool = False

marie = User(name='Marie', age=29, married=True)
fredrick = User(name='Fredrick', age=17)  # 'married' is False by default

这在TypedDict 中是不可能的(至少不需要定义多个这样的TypedDict 类型)。但是现在,你可以把任何字段标记为NotRequired ,以示字典中没有这个字段也没关系。

from typing import TypedDict, NotRequired

class User(TypedDict):
    name: str
    age: int
    married: NotRequired[bool]

marie: User = {'name': 'Marie', 'age': 29, 'married': True}
fredrick : User = {'name': 'Fredrick', 'age': 17}  # 'married' is not required

NotRequired 当你的字典中的大多数字段都是必需的时候,有几个不必需的字段是很好的。但是,对于相反的情况,你可以告诉 把每一个字段都默认为不需要,然后用 来标记实际需要的字段。TypedDict``Required

例如,这与前面的代码相同。

from typing import TypedDict, Required

# `total=False` means all fields are not required by default
class User(TypedDict, total=False):
    name: Required[str]
    age: Required[int]
    married: bool  # now this is optional

marie: User = {'name': 'Marie', 'age': 29, 'married': True}
fredrick : User = {'name': 'Fredrick', 'age': 17}  # 'married' is not required

contextlib.chdir

contextlib 有一个小的补充,这就是一个叫做 的上下文管理器。它所做的就是将当前工作目录改为上下文管理器内的指定目录,并在退出时将其设置回之前的状态。chdir

一个潜在的用例是重定向你写日志的地方。

import os

def write_logs(logs):
    with open('output.log', 'w') as file:
        file.write(logs)


def foo():
    print("Scanning files...")
    files = os.listdir(os.curdir)  # lists files in current directory
    logs = do_scan(files)

    print("Writing logs to /tmp...")
    with contextlib.chdir('/tmp'):
        write_logs(logs)

    print("Deleting files...")
    files = os.listdir(os.curdir)
    do_delete(files)

这样,你就不必担心手动改变和恢复当前目录,上下文管理器会帮你做到这一点。

总结

除了所有这些之外,还有一颗樱桃在上面。在这个版本中,Python 平均速度提高了 22%。当最终版本在10月左右发布时,它甚至可能会更快!

另外,Python 3.12 的工作已经开始。如果你想了解该语言的所有最新进展,你可以查看Python的GitHub仓库中的拉动请求页面。