器→工具, 编程语言

PEP 492:使用 async 和 await 语法的协程

钱魏Way · · 0 次浏览

PEP 492,全称为 “Coroutines with async and await syntax”,是 Python 编程语言中的一个重要提案。这个提案由 Yury Selivanov 在 2015 年提交,并最终成为了 Python 3.5 版本的一部分。PEP 492 引入了 async 和 await 两个关键字,为 Python 异步编程带来了重大的变革。以下是关于 PEP 492 的详细介绍:

PEP 492 的主要内容

  • 引入 async 和 await 关键字:
    • async def:用于定义协程(coroutine)。通过async def 定义的函数,当被调用时,不会立即执行,而是返回一个协程对象。
    • await:用于在协程内部暂停执行,直到等待的协程完成。它替换了旧的yield from 语法,提供了更清晰和直观的方式来处理协程。
  • 协程对象:
    • 由async def 定义的函数调用所返回的对象。这些对象并不会立即执行,而是需要在事件循环中被调度执行。
  • 异步上下文管理器和异步迭代器:
    • async with和 async for 语法的支持,允许在异步操作中使用上下文管理器和迭代器,进一步简化异步代码的编写。

PEP 492 的目的

  • 简化异步编程:通过引入async 和 await,使得编写异步代码变得更加简单和直观,特别是对于进行大量 IO 操作的应用。
  • 提高性能:异步编程模型在 IO 密集型应用中可以显著提高性能,尤其适用于网络编程和处理高并发场景。

对 Python 异步编程的影响

PEP 492 对 Python 的异步编程产生了深远的影响。它使得 async 和 await 成为编写异步代码的标准方法,极大地提升了代码的可读性和维护性。这一变化对 Python 中的异步框架和库,如 asyncio、aiohttp、FastAPI 等,都有着重要的意义。

摘要

Internet 和一般连接的增长引发了对快速响应和可伸缩代码的相应需求。这个建议旨在通过使显示地编写异步、并发的 Python 代码更简单、更符合 Python 风格来满足这一需求。

建议在 Python 中使协程成为一个合适的独立概念,并引入新的支持语法。最终目标是帮助在 Python 中建立一个通用的、更容易接近的、可理解的异步编程模型,并使其尽可能接近同步编程。

此 PEP 假定异步任务由一个类似于标准模块 asyncio.events.AbstractEventLoop 的事件循环来调度和协调。虽然 PEP 没有绑定到任何特定的事件循环实现,但是它只与使用 yield 作为调度器信号的协程相关,这表明协程将等待直到完成。

我们相信这里提出的更改将有助于保持 Python 在快速增长的异步编程领域的相关性和竞争力,因为许多其他语言已经或正在计划采用类似的特性。

API 设计和实现修订

  • Python 3.5 最初的 beta 版的反馈导致了对象模型的重新设计,该对象模型支持此 PEP,以便更清晰地将原生协程与生成器分离开来 —— 原生协程不再是一种新的生成器,而是与生成器完全不同的一种类型。此更改主要是由于试图将对原生协程的支持集成到 Tornado Web 服务器时遇到的问题而实现的。
  • 在 CPython 3.5.2 中, 更新了,__await__ 协议。在5.2 之前,__aiter__ 被期望返回一个能解析为异步迭代器的可等待对象。从 3.5.2 开始,__aiter__ 应该直接返回异步迭代器。如果在 3.5.2 中使用旧协议,Python 将发出一个 PendingDeprecationWarning。在 CPython 3.6 中,旧的 __aiter__ 协议仍然支持抛出一个 DeprecationWarning。在 CPython 3.7 中,旧的 __aiter__ 协议将不再受支持:如果 __aiter__ 返回的不是异步迭代器,则会引发RuntimeError。

基本原理和目标

当前 Python 支持通过生成器实现协程 (PEP 342),通过 PEP 380 中引入的 yield from 语法进一步增强了这一功能。但是这种方法有一些缺点:

  • 协程与常规生成器很容易混淆,因为它们具有相同的语法;对于新开发人员来说尤其如此。
  • 一个函数是否是协程是由它的主体中的 yield 或 yield from 语句决定的,当这些语句在重构过程中出现或消失时,可能会导致不明显的错误。
  • 对异步调用的支持仅限于语法上允许 yield 的表达式,这限制了语法特性(例如 with 和 for 语句)的有用性。

这个建议使协程成为原生 Python 语言的特性,并将它们与生成器明确地分开。这消除了生成器/协程的模糊性,并使可靠地定义协程成为可能,而不依赖于特定的库。这也使得 linters 和 IDE 能够改进静态代码分析和重构。

原生协程和相关的新语法特性使得以异步术语定义上下文管理器和迭代协议成为可能。如本建议后面所示,新的 async with 语句允许 Python 程序在进入和退出运行时上下文时执行异步调用,新的 async for 语句使得在迭代器中执行异步调用成为可能。

规范

这个建议引入了新的语法和语义来增强 Python 中对协程的支持。该规范假定您了解 Python 中协程的实现(PEP 342 和 PEP 380)。这里提出的语法更改的动机来自 asyncio 框架 (PEP 3156)和 Cofunctions 建议(PEP 3152,现在由于支持该规范而被拒绝)。从现在开始,我们使用 原生协程 这个词来引用使用新语法声明的函数。在需要引用基于生成器语法的协程时,可以使用 基于生成器的协程。协程 用于两个定义都适用的上下文中。

新的协程声明语法

下面新的语法用于声明一个原生协程:

async def read_data(db):
    pass

协程的主要特性:

  • 使用 async def 定义的函数总是协程,即使它们没有包含 await 表达式。
  • 在 async 函数中出现 yield 或 yield from 表达式将会引发 SyntaxError 异常。
  • 在内部,引入了两个新的代码对象标志:
    • CO_COROTINE 用于标记原生协程(使用新语法定义的协程)。
    • CO_ITERABLE_COROUTINE 用于使基于生成器的协程与原生协程兼容(由coroutine() 函数设置)。
  • 常规的生成器在调用时返回一个生成器对象。类似的、协程在调用时返回一个协程对象。
  • StopIteration 异常不会从协程中传播出去,而是用 RuntimeError 替换。对于常规生成器,这种行为需要导入 __futrue__ 模块(请参阅 PEP 479)。
  • 当原生协程被垃圾回收时,如果它从未被等待,则会引发 RuntimeWarning 异常(请参阅 [调试功能](# 4.7 调试功能))。
  • 更多信息请参阅 [协程对象](# 4.6 协程对象) 部分。

types.coroutine()

在 types 模块中添加了一个新的函数 coroutine(fn)。该函数允许 asyncio 模块中已存在的基于生成器的协程与本建议中介绍的原生协程可进行互操作。

@types.coroutine
def process_data(db):
    data = yield from read_data(db)
    ...

这个函数将 CO_ITERABLE_COROUTINE 标志应用到生成器函数的代码对象,使其返回一个协程对象。如果 fn 不是一个生成器函数,coroutine 函数会包装它;如果 fn 函数返回一个生成器,它将被包装在一个可等待代理对象中(请参阅下面可等待对象的定义)。

注意,types.coroutine() 没有应用 CO_COROUTINE 标志,以便能够将使用新语法定义的原生协程与基于生成器的协程区分开来。

await 表达式

下面的 await 表达式用于获得协程执行的结果:

async def read_data(db):
    data = await db.fetch('SELECT ...')
    ...

await (与 yield from 类似)将 read_data 协程的执行挂起,直到 db.fetch 可等待对象完成并返回结果数据。await 使用 yield from 实现,并附加一个用来验证参数的步骤。await 只接受一个可等待对象,它可以是:

  • 从原生协程函数返回的原生协程对象。
  • 从被coroutine() 装饰器装饰的函数中返回的基于生成器的协程对象。
  • 包含返回一个迭代器的 __await__ 方法的对象。
    • 所有 yield from 调用链均以一个 yield 结束,这是实现 Future 的基本机制。由于在内部,协程是一种特殊的生成器,所以每个 await 都被 await 调用链上某个位置的 yield 挂起(详细说明请参阅 PEP 3156)。
    • 为了支持协程的这种行为,添加了一个名为 __await__ 的魔法方法。例如,在 asyncio 中,为了在 await 语句中允许 Future 对象,惟一的更改是将 __await__ = __iter__ 这一行代码添加到Future 类 中。
    • 在这个 PEP 的其余部分中,使用 __await__ 方法的对象被称为 Future-like 对象。
    • 如果 __await__ 返回的不是迭代器,而是其他任何东西,那么将会引发 TypeError。
  • 使用带有am_await 函数的 CPython C API 定义的对象,返回一个迭代器(类似于 __await__ 方法)。

操作符优先级的更改

await 关键字的定义如下:

power ::=  await ["**" u_expr]
await ::=  ["await"] primary

其中 primary 表示语言中绑定最紧密的操作。它的语法是:

primary ::=  atom | attributeref | subscription | slicing | call

await 与 yield 和 yield from 操作符的关键区别在于,await 表达式在大多数情况下不需要括号。同样地, yield from 允许任何表达式作为他的参数,包括像 yield from a() + b() 这样的表达式。这个表达式将会被转换为 yield from (a() + b()) 。一般来说,任何算术运算的结果都不是一个可等待对象。为了避免这种错误,我们规定,await 的优先级低于 []、() 和 .,但是高于 **。

操作符 描述
yield x, yield from x yield 表达式
lambda lambda 表达式
If – else 条件表达式
or 布尔 OR
and 布尔 AND
not x 布尔 NOT
in, not in, is, is not, <, <=, >, >=, !=, == 比较,包括成员测试和身份测试
| 按位或
^ 按位异或
& 按位与
<<, >> 移位
+, – 加、减
*, @, /, //, % 乘法、矩阵乘法、除法、整除、取余
+x, -x, ~x 正、负、按位取反
** 求幂
await x await 表达式
x[index], x[index:index], x(arguments…), x.attribute 下标、切片、调用、属性引用
(expressions…), [expressions…], {key: value…}, {expressions…} 绑定或元组显示、列表显示、字典显示、集合显示

await 表达式的例子

合法的语法示例:

表达式 将被转化为
if await fut: pass If (await fut): pass
if await fut + 1: pass If (await fut) + 1: pass
pari = await fut, ‘spam’ pair = (await fut), ‘spam’
with await fut, open(): pass with (await fut), open(): pass
await foo()[‘spam’].baz()() await ( foo[‘spam’].baz()() )
return await coro() return ( await coro() )
res = await coro() ** 2 res = (await coro()) ** 2
func(a1=await coro(), a2=0) func(a1=(await coro()), a2=0)
await foo() + await bar() (await foo()) + (await bar())
-await foo() -(await foo())

不合法的语法示例:

表达式 应该被写为
await await coro() await (await coro())
await -coro() await (-coro())

异步上下文管理器和 “async with”

异步上下文管理器是能够在其 enter 和 exit 方法中暂停执行的上下文管理器。为此,提出了一种新的异步上下文管理器协议。添加了两个新的魔法方法:__aenter__ 和 __aexit__。两者都必须返回可等待对象。一个异步上下文管理器的例子:

class AsyncContextManager:
    async def __aenter__(self):
        await log('entering context')

    async def __aexit__(self, exc_type, exc, tb):
        await log('exiting context')

新语法

提出了一种新的异步上下文管理器语句:

async with EXPR as VAR:
    BLOCK

其语义等价于:

mgr = (EXPR)
aexit = type(mgr).__aexit__
aenter = type(mgr).__aenter__

VAR = await aenter(mgr)
try:
    BLOCK
except:
    if not await aexit(mgr, *sys.exc_info()):
        raise
else:
    await aexit(mgr, None, None, None)

与常规的 with 语句一样,可以在一个 async with 语句中指定多个上下文管理器。

将没有使用 __aenter__ 和 __aexit__ 方法的常规上下文管理器传递给 async with 是错误的。在 async def 函数外部使用 async with 将会引发 SyntaxError 异常。

示例

使用异步上下文管理器,很容易为协程实现适当的数据库事务管理器:

async def commit(session, data):
    ...

    async with session.transaction():
        ...
        await session.update(data)
        ...

需要锁定的代码看起来也更轻巧:

使用:

async with lock:
    ...

代替:

with (yield from lock):
    ...

异步迭代器和 “async for”

异步可迭代对象可以在其 iter 实现中调用异步代码,并且异步迭代器可以在其 next 方法中调用异步代码。为了支持异步迭代:

  • 对象必须实现 __await__ 方法(或者使用 CPython C API 的am_aiter 槽),该方法需要返回一个异步迭代器对象。
  • 异步迭代器对象必须实现一个 __anext__ 方法(或者使用 CPython C API 的am_anext 槽),该方法应该返回一个可等待对象。
  • 要停止迭代, __anext__ 方法必须引发一个 StopAsyncIteration 异常。

一个异步可迭代对象的例子:

class AsyncIterable:
    def __aiter__(self):
        return self

    async def __anext__(self):
        data = await self.fetch_data()
        if data:
            return data
        else:
            raise StopAsyncIteration

    async def fetch_data(self):
        ...

新语法

提出了一种新的异步迭代器迭代语句:

async for TARGET in ITER:
    BLOCK
else:
    BLOCK2

其语义等价于:

iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
    try:
        TARGET = await type(iter).__anext__(iter)
    except StopAsyncIteration:
        running = False
    else:
        BLOCK
else:
    BLOCK2

将未实现 __aiter__ 方法的常规可迭代对象传递给 async for 将会引发 TypeError 异常。 在 async def 函数外部使用 async for 将会引发 SynataxError 异常。

与常规的的 for 语句一样, async for 语句也有一个可选的 else 子句。

示例 1

使用异步迭代协议,可以在迭代期间异步缓冲数据:

async for data in cursor:
    ...

其中,cursor 是一个异步迭代器,它在每 N 次迭代之后从数据库中预取 N 行数据。

下面的代码演示了新的异步迭代协议:

class Cursor:
    def __init__(self):
        self.buffer = collections.deque()

    async def _prefetch(self):
        ...

    def __aiter__(self):
        return self

    async def __anext__(self):
        if not self.buffer:
            self.buffer = await self._prefetch()
            if not self.buffer:
                raise StopAsyncIteration
        return self.buffer.popleft()

可以像下面这样使用 Cursor 类:

async for row in Cursor():
    print(row)

上述代码与下面的代码是等价的:

i = Cursor().__aiter__()
while True:
    try:
        row = await i.__anext__()
    except StopAsyncIteration:
        break
    else:
        print(row)

示例 2

下面是一个实用程序类,它将常规迭代器转换为异步迭代器。虽然这样做不是很有用,但是代码说明了常规迭代器和异步迭代器之间的关系。

class AsyncIteratorWrapper:
    def __init__(self, obj):
        self._it = iter(obj)

    def __aiter__(self):
        return self

    async def __anext__(self):
        try:
            value = next(self._it)
        except StopIteration:
            raise StopAsyncIteration
        return value

async for letter in AsyncIteratorWrapper("abc"):
    print(letter)

为什么使用 StopAsyncIteration

在内部,协程仍然是基于生成器实现的。所以,在 PEP 479 之前

def g1():
    yield from fut
    return 'spam'

def g2():
    yield from fut
raise StopIteration('spam')

没有本质区别。

由于 PEP 479 已经被采纳并且为协程默认启用,下面的示例将把它的 StopIteration 封装到一个 RuntimeError中:

async def a1():
    await fut
    raise StopIteration('spam')

告诉外部代码迭代已经结束的唯一方法是引发 StopIteration 之外的其他异常。因此,添加了一个新的内置异常类StopAsyncIteration。

此外,使用 PEP 479 中的语义,协程中引发的所有 StopIteration 异常都封装在 RuntimeError 中。

协程对象

协程对象与生成器的区别

本节的内容仅适用于含有 CO_COROUTINE 标志的原生协程,例如,使用 async def 语法定义的协程。

  • asyncio 中现存的基于生成器的协程的行为仍然保持不变。为了确保将协程和生成器视为不同的概念,我们付出了巨大的努力:
  • 原生协程对象不实现 __iter__ 和 __next__ 方法。因此,它们不能被遍历或者传递给 iter()、list()、tuple() 和其他内置方法。他们也不能被用于..in 循环中。
  • 普通生成器不能 yield from 原生协程:这样做将会引发 TypeError 异常。
  • 基于生成器的协程(对于 asyncio 代码必须使用 @asyncio.coroutine 进行装饰)可以 yield from 原生协程对象。
  • isgenerator() 和 inspect.isgeneratorfunction() 对于原生协程对象和原生协程函数将会返回 False。

协程对象方法

由于协程内部基于生成器,因此它们共享实现。与生成器对象类似,协程也有 throw()、send() 和 close() 方法。StopIteration 和 GeneratorExit 在协程中扮演相同的角色(尽管默认情况下为协程启用了 PEP 479 )。有关详细信息,请参阅 PEP 342, PEP 380 和 Python文档。

协程的 send() 方法用于将值推送到 Future-like 对象,throw() 方法用于在 Future-like 对象中引发异常。

调试功能

初学者经常犯的一个错误是忘记在协程中使用 yield from:

@asyncio.coroutine
def useful():
    asyncio.sleep(1) # this will do nothing without 'yield from'

为了调试这类错误,asyncio 中有一个特殊的调试模式,在这个模式中 @coroutine 装饰器用一个特殊的对象包装所有函数,这个对象中有一个析构函数用来记录一个警告。无论何时被装饰的生成器被垃圾回收,都会生成详细的日志消息,其中包含关于装饰器函数的确切定义位置、收集位置的堆栈跟踪等信息。装饰器对象还提供了一个方便的带有关于生成器的详细信息的 __repr__ 函数。

唯一的问题是如何启用这些调试功能。由于调试工具在生产模式下应该是无操作的,所以 @coroutine 装饰器根据操作系统环境变量 PYTHONASYNCIODEBUG 来决定是否进行装饰。通过这种方式,可以运行 asyncio 程序,并检测 asyncio 自己的函数。EventLoop.set_debug() 是另一个不同的调试工具,该工具对 @coroutine 装饰器的行为没有影响。

在这个建议中,协程是一个原生概念,与生成器不同。除了在从未等待过的协程上引发 RuntimeWarning 之外,还建议向 sys 模块添加两个新函数:set_coroutine_wrapper 和 get_coroutine_wrapper。这是为了在 asyncio 和其他框架中启用高级调试工具(例如显示协程的确切创建位置,以及关于垃圾回收位置的更详细的堆栈跟踪)。

新的标准库函数

  • coroutine(gen)。请查看 types.coroutine() 了解详细信息。
  • iscoroutine(obj)。当obj 为原生协程对象时返回 True。
  • iscoroutinefunction(obj)。当obj 为原生协程函数时返回 True。
  • isawaitable(obj)。当obj 是一个可等待对象时返回 True。
  • getcoroutinestate(coro)。返回协程对象的当前状态。
  • getcoroutinelocals(coro)。返回原生协程对象的本地变量和其值的映射。
  • set_coroutine_wrapper(wrapper)。允许拦截原生协程对象的创建。wrapper 必须是接受一个参数(一个协程对象)的可调用对象或者 None。None 将重置装饰器。如果调用两次,新的装饰器将替换之前的装饰器。该函数是特定于线程的。有关详细信息,请参阅 [调试功能](# 4.7 调试功能) 章节的内容了解详细信息。
  • get_coroutine_wrapper()。返回当前的装饰器对象。如果未设置装饰器,则返回 None。该函数是特定于线程的,请参阅 [调试功能](# 4.7 调试功能) 章节的内容了解详细信息。

新的抽象基类

为了允许更好地与已存在的框架(例如 Tornado)集成和编译,新增了两个抽象基类:

  • abc.Awaitable ,该抽象基类用于 Future-like 对象,实现了 __await__ 方法。
  • abc.Coroutine,该抽象基类用于 coroutine 对象,实现了 send(value)、throw(type, exc, tb)、close() 和 __await__ 方法。

注意,基于生成器的带有 CO_ITERABLE 标志的协程不实现 __await__ 方法,因此它们不是 collections.abc.Coroutine 和 collections.abc.Awaitable 抽象基类的实例。

@types.coroutine
def gencoro():
    yield

assert not isinstance(gencoro(), collections.abc.Coroutine)

# however:
assert inspect.isawaitable(gencoro())

为了方便测试对象是否支持异步迭代,添加了另外两个抽象基类:

  • abc.AsyncIterable —— 测试 __aiter__ 方法。
  • abc.AsyncIterator —— 测试 __anext__ 方法。

术语

  • 原生协程函数。协程函数使用 async def 定义。它使用 await 和 return value。
  • 原生协程。从原生协程函数中返回的对象。
  • 基于生成器的协程函数。基于生成器语法的协程。最常见的例子是使用 @asyncio.coroutine 装饰的函数。
  • 基于生成器的协程。从基于生成器的协程函数返回的对象。
  • 协程。原生协程或基于生成器的协程
  • 协程对象。原生协程对象或基于生成器的协程对象
  • Future-like 对象。带有 __await__ 方法的对象或带有 tp_as_async->am_await 函数并返回迭代器的的 C 对象。可由协程中的 await 表达式使用。等待 Future-like 对象的协程被挂起,直到 Future-like 对象的 __await__ 完成并返回结果。
  • 可等待对象。Future-like 对象或协程对象。
  • 异步上下文管理器。异步上下文管理器有 __aenter__ 和 __aexit__ 方法,可以与 async with 一起使用。
  • 异步可迭代。带有 __aiter__ 方法的对象。该方法必须返回一个异步迭代器对象,可与 async for 一起使用。
  • 异步迭代器。异步迭代器带有一个 __anext__ 方法。

过度计划

为了避免与 async 和 await 的向后兼容性问题,决定修改 tokenizer.c,方式如下:

  • 识别 async def NAME 标记组合。
  • 在对 async def 块进行标记化时,用 ASYNC 替换 ‘async’ NAME 标记,用 AWAIT 替换 ‘await NAME 标记。
  • 在对 def 块进行标记化时,它按原样产出 ‘async’ 和 ‘await’ NAME 标记。

这种方法允许将新的语法特性(所有这些特性仅在异步函数中可用)与任何现有代码无缝地组合在一起。

一个包含 async def 和 async 属性的例子:

class Spam:
    async = 42

async def ham():
    print(getattr(Spam, 'async'))

# The coroutine can be executed and will print '42'

向后兼容

这个建议保持了100%的向后兼容性。

asyncio

对 asyncio 模块进行了调整和测试,使其能够使用协程和新语句。保留100%的向后兼容性,即所有现有的代码将按原样工作。

所需的改动主要有:

  • 修改 @asyncio.coroutine 装饰器,使其使用新的coroutine() 函数。
  • 在Future 类中添加 __await__ == __iter__ 这一行。
  • 添加 ensure_future() 作为 async() 函数的别名。废弃 async() 函数。

asyncio 合并策略

由于普通生成器不能 yield from 原生协程对象(有关详细信息,请参阅 与生成器部分的区别 ),因此建议确保在开始使用新语法之前,所有基于生成器的协程都用 @asyncio.coroutine 装饰。

CPython 代码库中的 async/await

在 CPython 中没有使用 await 名称。async 主要由 asyncio 使用。我们通过将 async() 函数重命名为 ensure_future() 来解决这个问题。另一个使用 async 关键字的地方是在 Lib/xml/dom/xmlbuilder.py 中,它为 DocumentLS 类定义了一个 async = False 属性。这段代码没有文档或测试,在 CPython 的其他任何地方都没有使用它。它被替换为 getter ,并且会引发一个 DeprecationWarning ,建议使用 async_ 属性代替。async 属性没有文档化,也没有在 CPython 代码库中使用。

语法更新

语法变化非常小:

decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef

compound_stmt: (if_stmt | while_stmt | for_stmt | try_stmt | with_stmt
                | funcdef | classdef | decorated | async_stmt)

async_stmt: ASYNC (funcdef | with_stmt | for_stmt)

power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*

废弃计划

async 和 await 名称将在 CPython 3.5 和 3.6 中被温和地弃用。在 Python 3.7 中,我们将把它们转换成合适的关键字。在 Python 3.7之前让 async 和 await 成为正确的关键字可能会使人们更难将代码移植到 Python 3。

设计考虑

PEP 3152

鉴于 PEP 3152 已被拒绝,在此就不再赘述。

协程-生成器

对于 async for 关键字,我们希望有一个协程生成器的概念——一个具有 yield 和 yield from 表达式的协程。为了避免与常规生成器的任何歧义,我们可能需要在 yield 之前有一个 async 关键字,而 async yield from 将引发一个StopAsyncIteration 异常。

虽然可以实现协程生成器,但我们认为它们超出了本建议的范围。这是一个应该仔细考虑和平衡的高级概念,在当前生成器对象的实现中有一些重要的更改。这是一个单独的 PEP 问题。

为什么使用 “async” 和 “await” 关键字

async/await 在编程语言中已经不是一个新的概念了:

  • C# 在很久以前就有了。
  • 提议在 ECMAScript 7 中新增 async/await 关键字。
  • Facebook 的 Hack/HHVM。
  • Google 的 Dark 语言。
  • Scala
  • 提议在 C++ 中新增 async/await 关键字。
  • 许多其他不太流行的语言。

这是一个巨大的好处,因为一些用户已经有了使用 async/await 的经验,而且因为它使在一个项目中使用多种语言变得更容易(例如使用ECMAScript 7 的 Python)。

为什么 __aiter__ 不返回一个可等待对象

该提议在 CPython 3.5.0 中被接受,其中 __aiter__ 被定义为一个方法,该方法被期待返回一个可解析为异步迭代器的可等待对象。

在 Python 3.5.2中(由于临时接受了该提议),对 __aiter__ 协议进行了更新,以直接返回异步迭代器。

这一更改背后的动机是使得使用 Python 实现异步生成器成为可能。

async 关键字的重要性

虽然可以只实现 await 表达式并将所有至少有一个 await 的函数视为协程,但是这种方法使得 API 的设计、代码重构和它的长时间支持变得更加困难。

假设Python只有 await 关键字:

def useful():
    ...
    await log(...)
    ...

def important():
    await useful()

如果 userful() 函数被重构了,并且有人从中移除了所有的 await 表达式,那么这个函数将会成为一个常规的 Python 函数,从而使得所有依赖于它的代码(包括 important 函数)都会损坏。为了缓解这个问题,必须引入一个类似 @asyncio.coroutine 的装饰器。

为什么是 “async def”

对于一些人来说,使用 async name(): pass 语法可能比使用 async def name(): pass 更有吸引力。打字当然更容易。但另一方面,它打破了 async def、async with 和 async for 之间的对称性,其中 async 是一个修饰符,声明语句是异步的。它也更符合现有的语法。

为什么不是 “await for” 和 “await with”

async 是一个形容词,因此它是 语句限定符 关键字的更好选择。await for/with 表示某事正在等待 for 或 with 语句的完成。

为什么是 “async def” 而不是 “def async”

async 关键字是一个 语句限定符。一个很好的类比是来自其他语言的 “static”、“public”、“unsafe” 关键字。async for 是一个异步 for 语句,async with 是一个异步 with 语句,async def 是一个异步函数。

在主要的语句关键字后面加上 async 可能会引起一些混淆,比如 for async item in iterator 可以读作 “for each asynchronous item in iterator”(对于迭代器中的每一个异步项)。

在 def 、with 和 for 之前有 async 关键字也使语言语法更简单。而且 async def 在视觉上更好地将协程与常规函数区分开来。

为什么不导入 __future__

[过度计划](# 6. 过度计划) 章节解释了如何修改令牌以只在 async def 块中将 async 和 await 作为关键字。因此,async def 充当了模块级编译器声明(如 from __future__ import async_await)将充当的角色。

为什么魔术方法以 “a” 开始

新的异步魔术方法 __aiter__、__anext__、__aenter__ 和 __aexit__ 都以前缀 a 开始。另一种方案是使用 async 前缀,这样,__anext__ 就变成了 __async_next__。然而,为了使新的魔法方法与现有的方法保持一致,如__radd__ 和 __iadd__,决定使用一个更短的版本。

为什么不复用现有的魔术名称

关于新的异步迭代器和上下文管理器的另一个想法是重用现有的魔术方法,方法是在声明中添加 async 关键字:

class CM:
    async def __enter__(self): # instead of __aenter__
        ...

这种方法有以下缺点:

  • 不可能创建一个同时适用 with 和 async with 语句的对象。
  • 会破坏向后兼容性,因为在小于等于4 的 Python 版本中,没有什么可以阻止从 __enter__ 和/或 __exit__ 返回 Future-like 对象。
  • 该建议的主要观点之一是使原生协程尽可能简单和安全,从而明确地分离协议。

为什么不复用现有的 “for” 和 “with” 语句

现有的基于生成器的协程和这个建议背后的愿景是让用户更容易看到代码可能被挂起的位置。使用现有的 for和 with 语句来识别异步迭代器和上下文管理器将不可避免地创建隐式挂起点,这使得对代码进行推理变得更加困难。

推导式

可以提供异步推导式的语法,但是这个构造超出了这个 PEP 的范围。

异步 lambda 函数

可以提供异步 lambda 函数的语法,但是这个构造超出了这个 PEP 的范围。

性能

总体影响

该建议没有引入可观察到的性能影响。下面是 Python 官方基准测试集的输出:

python perf.py -r -b default ../cpython/python.exe ../cpython-aw/python.exe

[skipped]

Report on Darwin ysmac 14.3.0 Darwin Kernel Version 14.3.0:
Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64
x86_64 i386

Total CPU cores: 8

### etree_iterparse ###
Min: 0.365359 -> 0.349168: 1.05x faster
Avg: 0.396924 -> 0.379735: 1.05x faster
Significant (t=9.71)
Stddev: 0.01225 -> 0.01277: 1.0423x larger

The following not significant results are hidden, use -v to show them:
django_v2, 2to3, etree_generate, etree_parse, etree_process, fastpickle,
fastunpickle, json_dump_v2, json_load, nbody, regex_v8, tornado_http.

分词器修改

使用修改的分词器解析 Python 文件没有明显的放缓:解析一个12Mb文件(Lib/test/test_binop.py重复1000次)需要相同的时间。

async/await

使用下面的微基准测试来确定 “async” 函数和生成器之间的性能差异:

import sys
import time

def binary(n):
    if n <= 0:
        return 1
    l = yield from binary(n - 1)
    r = yield from binary(n - 1)
    return l + 1 + r

async def abinary(n):
    if n <= 0:
        return 1
    l = await abinary(n - 1)
    r = await abinary(n - 1)
    return l + 1 + r

def timeit(func, depth, repeat):
    t0 = time.time()
    for _ in range(repeat):
        o = func(depth)
        try:
            while True:
                o.send(None)
        except StopIteration:
            pass
    t1 = time.time()
    print('{}({}) * {}: total {:.3f}s'.format(
        func.__name__, depth, repeat, t1-t0))

结果是,没有明显的性能差异:

binary(19) * 30: total 53.321s
abinary(19) * 30: total 55.073s

binary(19) * 30: total 53.361s
abinary(19) * 30: total 51.360s

binary(19) * 30: total 49.438s
abinary(19) * 30: total 51.047s

注意,深度 19 意味着 1,048,575 次调用。

参考实现

参考实现可以在 这里 找到。

高级别的更改和新协议的列表

  • 定义协程的新语法:async def 和 new await 关键字。
  • 用于类 Future 对象的新的 __await__ 方法,以及 PyTypeObject 中的新的am_await 槽。
  • 异步上下文管理器的新语法:async with。以及与 __aenter__ 和 __aexit__ 方法相关的协议。
  • 异步迭代的新语法:async for。以及带有 __aiter__,__aexit__ 和新的内置异常 StopAsyncIteration 的关联协议。 PyTypeObject 中的新的am_aiter 和 tp_as_async.am_anext 槽。
  • 新的AST节点:AsyncFunctionDef,AsyncFor,AsyncWith,Await。
  • 新函数:set_coroutine_wrapper(callback),sys.get_coroutine_wrapper(),types.coroutine(gen),inspect.iscoroutinefunction(func),inspect.iscoroutine(obj),inspect.isawaitable(obj),inspect.getcoroutinestate(coro) ,inspect.getcoroutinelocals(coro)。
  • 代码对象的新的 CO_COROUTINE 和 CO_ITERABLE_COROUTINE 位标志。
  • 新的抽象基类:abc.Awaitable,collections.abc.Coroutine,collections.abc.AsyncIterable 和collections.abc.AsyncIterator。
  • C API更改:新的 PyCoro_Type(作为CoroutineType 暴露给Python)和PyCoroObject. PyCoro_CheckExact(* o) 测试 o 是否是原生协程。

尽管变化和新事物的列表并不短,但重要的是要理解,大多数用户不会直接使用这些功能。它旨在用于框架和库中,为用户提供方便使用的 API和明确的 async def,await,async for 和 async with 语法。

可工作的示例

本提议中提出的所有概念都实现了,可以进行测试。

import asyncio

async def echo_server():
    print('Serving on localhost:8000')
    await asyncio.start_server(handle_connection,
                               'localhost', 8000)

async def handle_connection(reader, writer):
    print('New connection...')

    while True:
        data = await reader.read(8192)

        if not data:
            break

        print('Sending {:.10}... back'.format(repr(data)))
        writer.write(data)

loop = asyncio.get_event_loop()
loop.run_until_complete(echo_server())
try:
    loop.run_forever()
finally:
    loop.close()

验收

该提议于 2015-05-11 被 Guido 接受。

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注