Python 现代开发学习笔记

(九)任务组、结果与异常传播

1. 把一组相关操作放进同一作用域

# 项目文件:demos/task_group_results.py
# async with 离开前等待组内任务结束,之后 result 读取已完成的值。
import asyncio


async def twice(value):
    await asyncio.sleep(0)
    return value * 2


async def main():
    async with asyncio.TaskGroup() as group:
        tasks = [group.create_task(twice(value)) for value in [20, 35, 45]]
    print([task.result() for task in tasks])


if __name__ == "__main__":
    asyncio.run(main())

输出 [40, 70, 90]。async with 与前面的 with 一样标出使用范围,但进入、退出动作可以进行异步等待。TaskGroup 离开前收集组内任务,因此可以在外面读取这些已完成的结果。

这里的 task.result() 只读取已经完成的值;任务尚未完成时会抛出 InvalidStateError,不会像线程池的 future.result() 那样阻塞等待。需要等待 Task 时使用 await task,得到执行机会后再继续调用者的下一句。

2. 一个任务失败后先收尾同组任务

# 项目文件:demos/task_group_failure.py
# 等待项启动后,另一个任务才抛错;任务组取消等待项,等 finally 完成后向外报告。
import asyncio


async def waiting(ready, trace):
    try:
        trace.append("等待项开始")
        ready.set()
        await asyncio.Event().wait()
    finally:
        trace.append("等待项收尾")


async def failing(ready, trace):
    await ready.wait()
    trace.append("失败项抛错")
    raise ValueError("书目格式错误")


async def main():
    ready, trace = asyncio.Event(), []
    try:
        async with asyncio.TaskGroup() as group:
            group.create_task(waiting(ready, trace))
            group.create_task(failing(ready, trace))
    except* ValueError as errors:
        print("收到错误", [str(error) for error in errors.exceptions])
    print(trace)


if __name__ == "__main__":
    asyncio.run(main())

asyncio.Event 用来通知同一循环中的任务:ready 发出后,失败项开始抛错。输出错误 书目格式错误,再看到 ['等待项开始', '失败项抛错', '等待项收尾']。任务组没有在收到第一次失败后把另一项留在后台不管。

TaskGroup 中有普通错误时,会取消尚未结束的同组任务,等待它们退出,再把错误组成 ExceptionGroup 向外传播。Python 3.12 中的 except* ValueError 匹配异常组里相应类型的成员;本例的 .exceptions 里只有一个直接的 ValueError。它不同于普通 except 接住单个调用错误。

失败先触发同组取消,等待项执行finally,随后组异常向外传播

3. 为独立任务保留逐项结果

有些任务要全部尝试,分别报告成功和失败,可以收集异常作为结果:

# 项目文件:demos/gather_errors.py
# return_exceptions=True 保留各项结果;调用者逐项决定如何显示或继续处理。
import asyncio


async def check_minutes(value):
    await asyncio.sleep(0)
    if value < 0:
        raise ValueError("分钟数为负")
    return value * 2


async def main():
    results = await asyncio.gather(
        check_minutes(20), check_minutes(-1), return_exceptions=True
    )
    for result in results:
        if isinstance(result, Exception):
            print("错误", result)
        else:
            print("结果", result)


if __name__ == "__main__":
    asyncio.run(main())

输出 结果 40 与 错误 分钟数为负。默认 gather 不把普通异常当作返回值,第一次异常会向调用者传播,但不会因此自动取消其他已经交给 gather 的任务。TaskGroup 则把同组任务的生命周期绑在一起。本章书目导入需要完整数据,选择任务组;独立批量检查可以另存逐项失败记录。

笔记正文与全部题解 · Ctrl / ⌘ K