接前面 Python装饰器、Python类型提示与注解、Python生成器与yield,继续整理Python基础知识。
对于(原生)协程,Python(3.5以及更新版本)给出的定义如下:
Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points. They can be implemented with the
async def
statement. See also PEP 492.
翻译过来就是,协程(conroutine)是比子程序(subroutine)更通用形式: 常规子程序在某一点进入并在另一点退出;而协程可以在许多不同的点进入、退出和恢复。 协程可以通过async def
语句来实现。
我们先捋一捋和这个协程相关的概念,排除干扰,而后再回到 async def
定义的协程。
捋一捋 ?
Python在协程上没少折腾,而且和生成器有些不清不楚。
例子
简单起见,还是先看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
5个函数分别是:
- func1:普通的函数
- func2:生成器(函数)。返回值是一个迭代器(也叫生成器迭代器,或生成器),是 迭代器 + (经典)协程的合体
- func3:(原生)协程。
async def
定义,且里面不能有yield。有没有await无所谓 - func4:异步生成器。
async def
定义,且必须有yield - func5:生成器协程
它们调用方式各异:
1 2 3 4 5 6 7 8 |
|
结果如下:
1 2 3 4 5 |
|
将上面函数分为五类,不是拍脑袋说的。这些函数的co_flags
不相同,你可以直接输出:
1 2 3 4 5 |
|
对照inspect查看可以发现秘密:
inspect.CO_GENERATOR
:32inspect.CO_COROUTINE
:128inspect.CO_ITERABLE_COROUTINE
:256inspect.CO_ASYNC_GENERATOR
:512inspect.CO_OPTIMIZED
:1inspect.CO_NEWLOCALS
:2
协程1?协程2? 协程3?
上面一个简短的例子,直接出来三个协程:
- 经典协程:就是生成器,个人认为是迭代器+(经典)协程的合体。不能被await驱动。
- 原生协程:就是一般意义协程,使用
async def
定义。被await驱动。 - 生成器协程:使用装饰器
types.coroutine
对生成器函数的进行装饰。可被await驱动
另外,
- 异步生成器:使用
async def
定义,也和原生协程也密切相关。
要理清它们的关系,或许,我应该放上这张图:
实际上,__iter__()
和 __await__()
返回值都是迭代器(python库中甚至某些类定义中,为了兼容,还有__iter__ = __await__
的语句,比如asyncio.Future)。生成器和协程本质上,就是后者去掉了用于实现迭代的__next__()
成员。所以我觉得:
1 |
|
或者
1 |
|
注:本文关注点是原生协程,其他内容只是简单带过。
经典协程-生成器
这个协程,是一种生成器函数,调用者可以通过send()发送数据,通过yield from可以委托其他子协程。但它不能被await驱动。为了和Python3.5 引入的原生协程区分,这个协程在《流畅的Python》一书中称为经典协程。
相关PEPs:
Python 2.2 引入生成器(函数)以及yield语句。生成器函数用于生成 迭代器,迭代器有__iter__
和__next__
成员。(Python3.5文档中称其为:生成器迭代器,代码中称其为生成器)。
Python 2.5 对生成器进行了增强,引入了send()
、throw()
、close()
等成员。 这个操作使其变成了 迭代器 + 协程 的合体。见PEP 342。
Python 3.3 对这个 迭代器 + 协程 的合体,继续进行增强,引入yield from
语法,使其可以可以其他子协程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
原生协程-协程
Python 3.5 引入了使用async def
定义的协程函数,内部可以使用await委托子协程,有些类似于yield from。
Python手册中直接用coroutine称呼这个协程。但为与经典协程区分,这个协程在PEP492中被称为原生协程(native coroutine)。
基于生成器的协程
Python 3.5 引入了 types.coroutine 和 asyncio.coroutine 两个装饰生成器函数的装饰器。用于将生成器与await关键词兼容。
Decorator to mark generator-based coroutines. This enables the generator use
yield from
to callasync def
coroutines, and also enables the generator to be called byasync def
coroutines, for instance using anawait
expression.
注意:asyncio.coroutine这个东西在Python 3.8 中已经废弃。Python 3.11 中已经将其移除!
1 2 3 4 5 6 7 |
|
types.coroutine目前还在(它和asyncio.coroutine有一定差异,也无需纠结了):
1 2 3 4 5 6 7 8 9 10 11 12 |
|
尽管如此,它不被认可为 coroutine function:
1 2 3 4 5 6 7 8 9 |
|
协程 Coroutine
回归正题,看看Python中的(原生)协程。
Python 3.5 引入了使用async def
定义的协程函数,await
以及async with
和 async for
表达式。
协程函数(coroutine function)与协程(coroutine)
与生成器(函数)和迭代器 的关系一样:生成器(函数)是用于产生迭代器的函数。使用async def
定义的是协程函数,它用于生成协程对象。
1 2 3 4 5 6 7 |
|
这个func3只是一个函数(协程函数),它自身并不是协程:
1 2 3 |
|
它的返回值是协程Coroutine类型。
协程Coroutine与Awaitable
不同于Generator从Iterator派生,Coroutine 是 Awaitable 派生类。
协程对象是awaitable对象。协程执行时,调用__await__()
获取一个迭代器,而后遍历该迭代器。
除了协程之外,asyncio中下面两个也都是awaitable对象(但没有从Awaitable派生,鸭子行为):
- asyncio.Task
- asyncio.Future
注意:
- Coroutine:只能被await一次
- Future:可以被多次await
代码
Awaitable 和 Coroutine的定义在_collections_abc.py
中,可以顺便看一眼:
- Awaitable:派生类需要实现
__await__()
- Coroutine:派生类需要实现
__await__()
、send()
、throw()
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
asyncio
async 和 await 是异步编程的API,本身不依赖于asyncio,也可以用于其他的异步框架。
asyncio 先使用了yield from机制,而后才适配到 async和 await 机制。所以资料有些乱。
- Python3.3 引入
yield from
- Python 3.4 引入 asyncio 模块
- Python 3.5 引入
async
与await
- Python 3.6 引入异步生成器和异步推导式
- Python 3.7 将
async
与await
确定为关键字
asyncio 提供了高层(high-level)API:用于执行协程、网络IO、IPC、子进程控制,任务队列等。asyncio 的底层API:提供了事件循环的管理,网络与子进程异步API,系统signal处理,Protocols机制、回调函数与async的桥梁 等。
asyncio东西有点多,只看几个最简单的例子,意思一下
例子1
比如,要执行一个协程,在Python3.7下:
1 2 3 4 5 6 7 8 |
|
在Python 3.11下,使用Runner的上下文管理器:
1 2 3 4 5 6 7 8 9 |
|
使用底层API(获取事件循环对象),get_event_loop()
从Python3.12起被废弃:
1 2 3 4 5 6 7 8 9 10 |
|
- 获取事件循环
- 运行事件循环
- 关闭事件循环
例子2
执行4个任务,每个任务都分别打印0~10数字,可以使用asyncio.gather()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
- 使用
async with
配合TaskGroup也可以(不用显式使用 await)
1 2 3 4 |
|
asyncio.create_task
将协程 封装为一个Task并调度其执行,并返回 Task 对象。
只使用create_task
,还可以这么写:
1 2 3 4 5 6 7 |
|
例子3
操作时间不可控,超时怎么控制?
wait_for()
可以指定超时时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
- 使用
async with
配合asyncio.timeout()
1 2 3 4 5 6 7 |
|
例子4
有阻塞操作,需要次线程,怎么处理?
- 使用
asyncio.to_thread()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
参考
- https://docs.python.org/3.11/glossary.html#term-coroutine
- https://docs.python.org/3.11/reference/datamodel.html#coroutines
- https://docs.python.org/3/library/asyncio-task.html
- https://en.wikipedia.org/wiki/Coroutine
- PEP 492 – Coroutines with async and await syntax
- PEP 342 – Coroutines via Enhanced Generators
- PEP 380 – Syntax for Delegating to a Subgenerator
- PEP 525 – Asynchronous Generators
- PEP 3156 – Asynchronous IO Support Rebooted: the “asyncio” Module
- https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/
- https://blog.allegro.tech/2022/01/how-do-coroutines-work-internally-in-python.html
- https://superfastpython.com/python-coroutine/
- https://realpython.com/async-io-python/