PEP525 -- 异步生成器

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 342PEP 380PEP 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 对异步迭代协议的支持

异步迭代协议需要实现两个特殊方法:

  1. 返回异步迭代器的 ___aiter__ 方法。
  2. 返回 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 解释器将无法执行任何操作。

为了解决这个问题,我们建议做以下工作:

  1. 实现异步生成器上的 aclose 方法,返回一个特殊的 awaitable。当等待时,它会在挂起的生成器中抛出 GeneratorExit 异常,并在其上迭代,直到发生 GeneratorExitStopAsyncIteration 异常。

    这与 close() 方法对常规 Python 生成器所做的非常相似,只是执行 aclose() 需要一个事件循环。

  2. 当异步生成器在它的 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.
    
  3. 在 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() 返回一个包含 firstiterfinalizer 字段的 “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 是异步的。

定义了以下方法和属性:

  1. agen.__aiter__():返回 agen

  2. agen.__anext__():返回一个 awaitable,它在等待时执行异步生成器迭代。

  3. 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.)
    
  4. 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.
    
  5. agen.aclose():返回一个 awaitable,向生成器中抛出 GeneatorExit 异常。如果 agen 处理了这个异常,这个 awaitable 也可以返回一个产出的值,否则 agen 将会被关闭,并且这个异常将会被传播到调用者。

  6. agen.__name__agen.__qualname__:可读写的 namequalname 属性。

  7. agen.ag_awaitagen 当前正在等待的对象,或者 None。这类似于当前可用的生成器的 gi_yieldfrom 和协程的 cr_wait

  8. agen.ag_frameagen.ag_runningagen.ag_code:以与标准生成器的类似属性相同的方式定义。

StopIterationStopAsyncIteration 异常不会从异步生成器中传播出去,而是用 RuntimeError 异常替换。

3.6 实现细节

异步生成器对象(PyAsyncGenObject)与 PyGenObject 共享结构布局。除此之外,参考实现还引入了三个新对象:

  1. PyAsyncGenASend:实现了 __anext__asend() 方法的可等待对象。

  2. PyAsyncGenAThrow:实现 athrow()aclose() 方法的可访问对象。

  3. _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 的实例(它保存对父代理对象的引用。)

数据流定义如下:

  1. 当第一次调用 PyAsyncGenASend.send(val) 时,val 被推送到父 agen 对象(使用 PyGenObject 的现有设施)。

    PyAsyncGenASend 对象的后续迭代,将 None 推送到 agen

    当产出 _PyAsyncGenWrappedValue 对象时,它将进行拆箱,并使用未包装的值作为参数引发 StopIteration 异常。

  2. 当第一次调用 PyAsyncGenASend.throw(*exc) 时,*exc 被抛出到父 agen 对象中。

    PyAsyncGenASend 对象的后续迭代,将 None 推送到 agen

    当产出 _PyAsyncGenWrappedValue 对象时,它将进行拆箱,并使用未包装的值作为参数引发 StopIteration 异常。

  3. 异步生成器中的 return 语句引发 StopAsyncIteration 异常,该异常通过 PyAsyncGenASend.send()PyAsyncGenASend.throw() 方法传播。

PyAsyncGenAThrow 与 PyAsyncGenASend 非常相似。惟一的区别是,第一次调用 PyAsyncGenAThrow.send() 时,将异常抛出给父 agen 对象(而不是将值推入其中)。

3.7 新标准库函数和类型

  1. types.AsyncGeneratorType :异步生成器对象。
  2. sys.set_asyncgen_hooks()sys.get_asyncgen_hooks() 方法用来设置事件循环中的异步生成器终结器和迭代拦截器。
  3. inspect.isasyncgen()inspect.isasyncgenfunction() 自省函数。
  4. loop.shutdown_asyncgens():asyncio 事件循环的新方法。
  5. 新的 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 日接受。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值