Python 3.11 你应该试试的超酷特性
Python 3.11在10月24日发布。它是Python最新版本,运行速度更快且更佳友好。在经过17个月的研发,终于到了可以使用的黄金时期。
和每个发布版本一样,Python 3.11做了大量的变更及提高。你可以通过查看文档来了解大部分内容。这里,我们会阐述最酷且最有影响力的新特性。
教程里将涵盖如下内容:
- 更好的错误信息来帮助代码追踪
- Faster CPython项目加速代码执行
- 在异步代码中使用Task和exception groups
- Python静态类型新增的几个类型特性
- 原生支持TOML格式配置文件
如果你想运行本教程所提供的示例文件,你需要先安装Python 3.11.
异步执行多任务语法改进
Python对于异步编程已经发展很长时间了。最早是在Python 2加入生成器的支持。Python 3.4又原生增加了asyncio库。
最近的发布版本仍然对该一块有开发,对Python异步能力做了许多改善。Python 3.11加入的task组的,提供了更简洁的语法来执行和监控异步任务。
传统方式使用asyncio时使用create_task()创建多个任务,并且await gather()等待任务执行完成。这种方式显得有点繁琐。
为组织多个子任务,Curio引入了task组的概念,Trio使用nurseries类似相同概念。asyncio task组大部分参考于它们。
当使用gather()来组织异步任务时,通常一部分代码类似如下:
tasks = [asyncio.create_task(run_some_task(param)) for param in params]
await asyncio.gather(*tasks)
我们手动将任务放入列表中传递给gather()。通过等待gather(),我们确保所有的任务均执行结束。
task组实现的代码更加直观,不用gather(),我们通过使用context manager来实现任务等待。
async with asyncio.TaskGroup() as tg:
for param in params:
tg.create_task(run_some_task(param))
这里创建了一个名为tg的task组对象,使用它的.create_task()方法来创建新的任务。
我们来看一个完整的例子,该例子同时下载多个文件。它尝试下载与异步特性相关的PEP文档。为避免重复造轮,我们这里使用的第三方库aiohttp来实现异步下载。
导入必要的包之后,我们标注好仓库URL里PEP文件名占位地方。
# download_peps_gather.py
import asyncio
from typing import Sequence
import aiohttp
PEP_URL = (
"https://raw.githubusercontent.com/python/peps/master/pep-{pep:04d}.txt"
)
async def main(peps: Sequence[str]):
async with aiohttp.ClientSession() as session:
await download_peps(session, peps)
main()函数使用aiohttp会话创建一个可重用的连接池。然后,调用download_peps()函数,这里我们还没有实现,该函数将会为每个PEP创建一个任务来进行下载。
# download_peps_gather.py
# ...
async def download_peps(session, peps: Sequence[str]):
tasks = [asyncio.create_task(download_pep(session, pep)) for pep in peps]
await asyncio.gather(*tasks)
接下来的就是之前所看到的代码了。每个任务使用我们后面会介绍的download_pep()来执行,一旦设置好了所有的任务,我们将所有任务传递给了gather()。
每个任务下载PEP,然后我们通过print()打印看看所发生了什么:
async def download_pep(session, pep: str):
print(f"Downloading PEP {pep}")
url = PEP_URL.format(pep=pep)
async with session.get(url) as response:
pep_text = await response.text()
title = pep_text.split("\n")[1].removeprefix("Title:").strip()
print(f"Downloaded PEP {pep}: {title}")
每个PEP生成独立的URL,然后使用session.get()来下载。一旦我们取得PEP的文本内容,我们看到了PEP的标题打印在终端。
最终,我们使用异步方式来执行main():
# download_peps_gather.py
# ...
asyncio.run(main([492, 525, 530, 3148, 3156]))
我们使用一个PEP编号的列表作为参数来执行结果如下:
$ python download_peps_gather.py
Downloading PEP 492
Downloading PEP 525
Downloading PEP 530
Downloading PEP 3148
Downloading PEP 3156
Downloaded PEP 3148: futures - execute computations asynchronously
Downloaded PEP 492: Coroutines with async and await syntax
Downloaded PEP 530: Asynchronous Comprehensions
Downloaded PEP 3156: Asynchronous IO Support Rebooted: the "asyncio" Module
Downloaded PEP 525: Asynchronous Generators
我们观察到所有下载发生在相同时间,因为所有的下载日志都在完成日志之前。同样,所有任务执行顺序与触发任务顺序相同,我们按照了PEP编号排序。
相反,任务完成时间是无序的。gather()确保所有的任务都已经执行完成。
我们可以将gather()方式替换为task组。首先,复制download_peps_gather.py为download_peps_taskgroup.py。大部分内容都相同,我们只需要修改download_peps()函数:
# download_peps_taskgroup.py
# ...
async def download_peps(session, peps: Sequence[str]):
async with asyncio.TaskGroup() as tg:
for pep in peps:
tg.create_task(download_pep(session, pep))
首先通过使用context manager生成一个task组,然后我们在task组里创建子任务,每个任务下载单个PEP。执行更新后的代码,观察是否输出结果和之前一致。
在执行多个异步任务时比较头疼的是在执行过程中引发的异常。理论上,同时还有可能还会抛出多个异常。
类似Trio和Curio使用一个multi-error对象来处理,但是用起来还是觉得有些繁琐,主要原因还是因为Python没有提供内置处理方式。
为了更好的处理task组里异常,Python 3.11引入exception groups来追踪多个并发错误。后面我们会具体讨论。
task组使用异常组提供了比旧式更好的处理机制。更多深入task组讨论,查看Python 3.11 Preview: Task and Exception Groups。你还可以学习一下其内部原理[Reasoning about asyncio.Semaphore](https:/