文章目录
1. 摘要
PEP 429 向 Python 3.5 中引入了对本地协程和 async/await
语法的支持。这个建议通过添加对异步生成器的支持来扩展 Python 的异步功能。
2. 原理和目标
常规生成器(在 PEP 255 中引入)启用了一种编写复杂数据生成器的优雅方式,并使它们的行为类似于迭代器。
然而,目前还没有对应的异步迭代协议(async for)概念。这使得编写异步数据生成器变得不必要的复杂,因为必须定义一个实现了 __aiter__
和 __anext__
的类,以便能够在 async for
语句中使用它。
本质上,应用于异步执行案例的 PEP 255 的目标和原理也适用于这个建议。
性能是这个建议的另一个要点:在我们对引用实现的测试中,异步生成器比作为异步迭代器实现的等效生成器快2倍。
作为代码质量改进的一个例子,考虑下面的类,它会在迭代后打印具有给定延迟的数字:
class Ticker:
"""Yield numbers from 0 to `to` every `delay` seconds."""
def __init__(self, delay, to):
self.delay = delay
self.i = 0
self.to = to
def __aiter__(self):
return self
async def __anext__(self):
i = self.i
if i >= self.to:
raise StopAsyncIteration
self.i += 1
if i:
await asyncio.sleep(self.delay)
return i
这个类也可以实现为一个更简单的异步生成器:
async def ticker(delay, to):
"""Yield numbers from 0 to `to` every `delay` seconds."""
for i in range(to):
yield i
await asyncio.sleep(delay)
3. 规范
这个提议向 Python 引入了异步生成器的概念。
该规范假定您了解 Python 中生成器和协程的实现(PEP 342、PEP 380 和 PEP 429 )。
3.1 异步生成器
Python 的生成器是任何包含一个或多个 yield
表达式的函数:
def func(): # 函数
return
def genfunc(): # 生成器函数
yield
我们的建议是使用同样的方式定义异步生成器:
async def coro(): # 协程函数
await smth()
async def asyncgen(): # 异步生成器函数
await smth()
yield 42
调用一个异步生成器函数将得到一个实现了异步迭代协议(在 PEP 429 中定义)的异步生成器对象。
在异步生成器中包含非空的 return
语句将引发 SyntaxError
异常。
3.2 对异步迭代协议的支持
异步迭代协议需要实现两个特殊方法:
- 返回异步迭代器的
___aiter__
方法。 - 返回
awaitable
对象的方法,它使用StopIteration
来 “产出” 值,使用StopAsyncIteration
异常来表示迭代的结束。
异步生成器定义了这两种方法。让我们手动遍历一个简单的异步生成器:
async def genfunc():
yield 1
yield 2
gen = genfunc()
assert gen.__aiter__() is gen
assert await gen.__anext__() == 1
assert await gen.__anext__() == 2
await gen.__anext__() # 这一行将会引发 StopAsyncIteration 异常.
3.3 异步生成器的终结
PEP 429 需要一个事件循环或调度程序来运行协程。因为异步生成器是要从协程中使用的,所以它们还需要一个事件循环来运行和完成它们。
异步生成器可以有 try..finally
块,以及 async for
。重要的是要提供一个保证,即使在进行部分迭代,然后进行垃圾回收时,最终也可以安全地确定生成器。例如:
async def square_series(con, to):
async with con.transaction():
cursor = con.cursor(
'SELECT generate_series(0, $1) AS i', to)
async for row in cursor:
yield row['i'] ** 2
async for i in square_series(con, 1000):
if i == 100:
break
上面的代码定义了一个异步生成器,它使用 async with
对事务中的数据库游标进行迭代。然后使用 async for
迭代生成器,它会在某个时候中断迭代。
然后,square_series()
生成器将被垃圾回收,如果没有异步关闭生成器的机制,Python 解释器将无法执行任何操作。
为了解决这个问题,我们建议做以下工作:
-
实现异步生成器上的
aclose
方法,返回一个特殊的awaitable
。当等待时,它会在挂起的生成器中抛出GeneratorExit
异常,并在其上迭代,直到发生GeneratorExit
或StopAsyncIteration
异常。这与
close()
方法对常规 Python 生成器所做的非常相似,只是执行aclose()
需要一个事件循环。 -
当异步生成器在它的
finally
块中执行一个yield
表达式(使用await
也是可以的)时引发RuntimeError
异常。async def gen(): try: yield finally: await asyncio.sleep(1) # Can use 'await'. yield # Cannot use 'yield', # this line will trigger a # RuntimeError.
-
在 sys 模块中新增两个方法 :
set_asyncgen_hooks()
和get_asyncgen_hooks()
。
set_asyncgen_hooks()
背后的思想是允许事件循环拦截异步生成器的迭代和终结,这样最终用户就不需要关心终结问题,一切都可以正常工作。
set_asyncgen_hooks()
接受两个参数:
firstiter
:一个可调用对象,当异步生成器第一次迭代时被调用。finalizer
:一个可调用的函数,当异步生成器即将被垃圾回收时调用。
当异步生成器第一次迭代时,它存储对当前 finalizer
的引用。
当异步生成器即将被垃圾回收时,它调用其缓存的 finalizer
。假设 finalizer
将调度一个 aclose()
调用,其中包含迭代开始时处于活动状态的循环。
例如,这里是如何修改 asyncio,以允许异步生成器的安全终结:
# asyncio/base_events.py
class BaseEventLoop:
def run_forever(self):
...
old_hooks = sys.get_asyncgen_hooks()
sys.set_asyncgen_hooks(finalizer=self._finalize_asyncgen)
try:
...
finally:
sys.set_asyncgen_hooks(*old_hooks)
...
def _finalize_asyncgen(self, gen):
self.create_task(gen.aclose())
第二个参数 firstiter
允许事件循环维护在其控制下实例化的一组弱异步生成器。这使得实现 “shutdown” 机制成为可能,从而安全地完成所有打开的生成器并关闭事件循环。
set_asyncgen_hooks()
是特定于线程的,因此在并行线程中运行的多个事件循环可以安全地使用它。
get_asyncgen_hooks()
返回一个包含 firstiter
和 finalizer
字段的 “namedtuple-like” 结构。
3.4 asynio
asyncio 事件循环将使用 sys.set_asyncgen_hooks()
API 来维护所有调度的异步生成器的弱集,并在需要对生成器进行垃圾回收时调度它们的 aclose()
协程方法。
为了确保 asyncio 程序能够可靠地完成所有预定的异步生成器,我们建议添加一个新的事件循环协程方法loop.shutdown_asyncgens()
。该方法将使用 aclose()
调用来调度所有当前打开的异步生成器的关闭。
在调用 loop.shutdown_asyncgens()
方法之后,每当第一次迭代一个新的异步生成器时,事件循环都会发出警告。其思想是,在请求关闭所有异步生成器之后,程序不应该执行在新的异步生成器上迭代的代码。
一个如何使用 shutdown_asyncgens
协同程序的例子:
try:
loop.run_forever()
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
3.5 异步生成器对象
该对象是按照标准 Python 生成器对象建模的。本质上,异步生成器的行为被设计为复制同步生成器的行为,唯一的区别在于 API 是异步的。
定义了以下方法和属性:
-
agen.__aiter__()
:返回agen
。 -
agen.__anext__()
:返回一个awaitable
,它在等待时执行异步生成器迭代。 -
agen.asend(val)
:返回一个awaitable
,它将val
对象推入agen
生成器。当agen
还没有被迭代时,val
必须为 None。示例:
async def gen(): await asyncio.sleep(0.1) v = yield 42 print(v) await asyncio.sleep(0.2) g = gen() await g.asend(None) # Will return 42 after sleeping # for 0.1 seconds. await g.asend('hello') # Will print 'hello' and # raise StopAsyncIteration # (after sleeping for 0.2 seconds.)
-
agen.athrow(typ, [val, [tb]])
:返回一个awaitable
,向agen
生成器中抛出一个异常。示例:
async def gen(): try: await asyncio.sleep(0.1) yield 'hello' except ZeroDivisionError: await asyncio.sleep(0.2) yield 'world' g = gen() v = await g.asend(None) print(v) # Will print 'hello' after # sleeping for 0.1 seconds. v = await g.athrow(ZeroDivisionError) print(v) # Will print 'world' after $ sleeping 0.2 seconds.
-
agen.aclose()
:返回一个awaitable
,向生成器中抛出GeneatorExit
异常。如果agen
处理了这个异常,这个awaitable
也可以返回一个产出的值,否则agen
将会被关闭,并且这个异常将会被传播到调用者。 -
agen.__name__
和agen.__qualname__
:可读写的 name 和qualname
属性。 -
agen.ag_await
:agen
当前正在等待的对象,或者 None。这类似于当前可用的生成器的gi_yieldfrom
和协程的cr_wait
。 -
agen.ag_frame
、agen.ag_running
和agen.ag_code
:以与标准生成器的类似属性相同的方式定义。
StopIteration
和 StopAsyncIteration
异常不会从异步生成器中传播出去,而是用 RuntimeError
异常替换。
3.6 实现细节
异步生成器对象(PyAsyncGenObject
)与 PyGenObject
共享结构布局。除此之外,参考实现还引入了三个新对象:
-
PyAsyncGenASend:实现了
__anext__
和asend()
方法的可等待对象。 -
PyAsyncGenAThrow:实现
athrow()
和aclose()
方法的可访问对象。 -
_PyAsyncGenWrappedValue:异步生成器中直接产出的每个对象都隐式地装箱到这个结构中。这就是生成器实现如何将使用常规迭代协议产出的对象与使用异步迭代协议产出的对象分离开来。
PyAsyncGenASend 和 PyAsyncGenAThrow 是可等待对象(它们有返回 self
的 __await__
方法),并且是 coroutine-like
对象(实现了 __iter__
、__next__
、send()
和 throw()
方法)。本质上,它们控制异步生成器的迭代方式:
[外链图片转存失败(img-QvNLhRcj-1565088453140)(assets/pep-0525-1.png)]
3.6.1 PyAsyncGenASend 和 PyAsyncGenAThrow
PyAsyncGenASend
是一个 coroutine-like
对象,它驱动 __anext__
和 asend()
方法并实现异步迭代协议。
agen.asend(val)
和 agen.__anext__()
返回 PyAsyncGenASend
的实例(它保存对父代理对象的引用。)
数据流定义如下:
-
当第一次调用
PyAsyncGenASend.send(val)
时,val 被推送到父agen
对象(使用PyGenObject
的现有设施)。对
PyAsyncGenASend
对象的后续迭代,将 None 推送到agen
。当产出
_PyAsyncGenWrappedValue
对象时,它将进行拆箱,并使用未包装的值作为参数引发StopIteration
异常。 -
当第一次调用
PyAsyncGenASend.throw(*exc)
时,*exc 被抛出到父agen
对象中。对
PyAsyncGenASend
对象的后续迭代,将 None 推送到agen
。当产出
_PyAsyncGenWrappedValue
对象时,它将进行拆箱,并使用未包装的值作为参数引发StopIteration
异常。 -
异步生成器中的
return
语句引发StopAsyncIteration
异常,该异常通过PyAsyncGenASend.send()
和PyAsyncGenASend.throw()
方法传播。
PyAsyncGenAThrow 与 PyAsyncGenASend 非常相似。惟一的区别是,第一次调用 PyAsyncGenAThrow.send() 时,将异常抛出给父 agen
对象(而不是将值推入其中)。
3.7 新标准库函数和类型
types.AsyncGeneratorType
:异步生成器对象。sys.set_asyncgen_hooks()
和sys.get_asyncgen_hooks()
方法用来设置事件循环中的异步生成器终结器和迭代拦截器。inspect.isasyncgen()
和inspect.isasyncgenfunction()
自省函数。loop.shutdown_asyncgens()
:asyncio 事件循环的新方法。- 新的
collections.abc.AsyncGenerator
抽象基类。
3.8 向后兼容
这个建议是完全向后兼容的。
在 Python 3.5中,定义一个包含 yield
表达式的 async def
函数将会引发 SyntaxError
,因此,在 Python 3.6 中引入异步生成器是安全的。
4. 性能
4.1 常规生成器
常规生成器没有性能下降。下面的微基准测试在有或没有异步生成器的 CPython 上的速度运行相同:
def gen():
i = 0
while i < 100000000:
yield i
i += 1
list(gen())
4.2 对异步迭代器的改进
下面的微基准测试表明异步生成器比纯 Python 中实现的异步迭代器快 2.3 倍:
N = 10 ** 7
async def agen():
for i in range(N):
yield i
class AIter:
def __init__(self):
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
i = self.i
if i >= N:
raise StopAsyncIteration
self.i += 1
return i
5. 设计考虑
5.1 aiter() 和 anext() 内置函数
最初,PEP 492 将 __aiter__
定义为一个应该返回一个 awaitable
对象的方法,从而产生一个异步迭代器。
然而,在 CPython 3.5.2 中,重新定义了 __aiter__
来直接返回异步迭代器。为了避免破坏向后兼容性,决定 Python 3.6 将支持两种方式:__aiter__
仍然可以返回一个 awaitable
,并发出一个 DeprecationWarning
警告。
由于 Python 3.6 中 __aiter__
的双重特性,我们不能添加内置的 aiter()
同步实现。因此,建议等到 Python3.7。
5.2 异步列表、字典、集合推导式
异步推导式的语法与异步生成器机制无关,应该在单独的 PEP 中考虑。
5.3 异步 yield from
虽然在理论上可以实现对异步生成器的 yield from
的支持,但这需要对生成器实现进行认真的重新设计。
对于异步生成器,yield from
也不是那么重要,因为不需要提供在协程之上实现另一个协程协议的机制。为了组成异步生成器,可以使用一个简单的 async for
循环:
async def g1():
yield 1
yield 2
async def g2():
async for v in g1():
yield v
5.4 为什么 asend() 和 athrow() 方法是必须的
它们使得使用异步生成器实现类似 contextlib.contextmanager
的概念成为可能。 。例如,使用建议的设计,可以实现以下模式:
@async_context_manager
async def ctx():
await open()
try:
yield
finally:
await close()
async with ctx():
await ...
另一个原因是,可以使用从 __anext__
返回的对象来推送数据并将异常抛出到异步生成器中,但是很难正确地做到这一点。添加显式的 asend()
和 athrow()
将为实现这一点铺平一条安全的道路。
在实现方面,``asend()是稍微通用一点的
anext版本,而
athrow()与
aclose()` 非常相似。因此,为异步生成器定义这些方法不会增加任何额外的复杂性。
6. 示例
使用当前参考实现的一个可工作的示例(将打印从0到9的数字,延迟1秒):
async def ticker(delay, to):
for i in range(to):
yield i
await asyncio.sleep(delay)
async def run():
async for i in ticker(1, 10):
print(i)
import asyncio
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(run())
finally:
loop.close()
7. 验收
该建议已经被 Guido 于 2016 年 9 月 6 日接受。