Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 492 – 使用 async 和 await 语法实现协程

作者:
Yury Selivanov <yury at edgedb.com>
讨论邮件列表:
Python-Dev 邮件列表
状态:
最终
类型:
标准跟踪
创建日期:
2015 年 4 月 9 日
Python 版本:
3.5
历史记录:
2015 年 4 月 17 日、2015 年 4 月 21 日、2015 年 4 月 27 日、2015 年 4 月 29 日、2015 年 5 月 5 日

目录

摘要

互联网和通用连接的增长,也带来了对响应式和可扩展代码的需求。本提案旨在通过使编写显式异步、并发 Python 代码更轻松、更 Pythonic 来满足这种需求。

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

本 PEP 假设异步任务由类似于标准库模块 asyncio.events.AbstractEventLoop 的事件循环来调度和协调。虽然 PEP 不依赖于任何特定的事件循环实现,但它仅与使用 yield 作为调度器信号的协程相关,该信号表示协程将在事件(如 IO)完成之前等待。

我们相信,这里提出的更改将有助于保持 Python 在快速发展的异步编程领域中保持竞争力,因为许多其他语言已经采用或计划采用类似的功能:[2][5][6][7][8][10].

API 设计和实现修订

  1. 对 Python 3.5 初始 beta 版本的反馈导致了支持本 PEP 的对象模型的重新设计,以便更清楚地将原生协程与生成器分离——原生协程不再是生成器的新类型,而是它们自己完全不同的类型(在 [17] 中实现)。

    此更改主要基于尝试将原生协程支持集成到 Tornado Web 服务器时遇到的问题(在 [18] 中报告)。

  2. 在 CPython 3.5.2 中,更新了 __aiter__ 协议。

    在 3.5.2 之前,预期 __aiter__ 返回解析为异步迭代器的可等待对象。从 3.5.2 开始,__aiter__ 应该直接返回异步迭代器。

    如果在 3.5.2 中使用旧协议,Python 将引发 PendingDeprecationWarning

    在 CPython 3.6 中,将继续支持旧的 __aiter__ 协议,并引发 DeprecationWarning

    在 CPython 3.7 中,将不再支持旧的 __aiter__ 协议:如果 __aiter__ 返回的不是异步迭代器,将引发 RuntimeError

    有关更多详细信息,请参阅 [19][20]

基本原理和目标

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

  • 协程很容易与普通生成器混淆,因为它们共享相同的语法;这对新手开发者来说尤其如此。
  • 函数是否是协程由其主体中是否存在 yieldyield from 语句决定,这会导致在重构期间此类语句出现在函数主体或从函数主体中消失时出现难以察觉的错误。
  • 异步调用的支持仅限于 yield 在语法上允许的表达式,这限制了语法特征(如 withfor 语句)的实用性。

本提案将协程作为 Python 语言的原生功能,并将其与生成器明确区分开来。这消除了生成器/协程的歧义,并使得能够在不依赖于特定库的情况下可靠地定义协程。这也使 linter 和 IDE 能够改进静态代码分析和重构。

原生协程和相关的新语法功能使得能够以异步方式定义上下文管理器和迭代协议。如本提案后面所示,新的 async with 语句使 Python 程序能够在进入和退出运行时上下文时执行异步调用,而新的 async for 语句使得能够在迭代器中执行异步调用。

规范

本提案引入了新的语法和语义,以增强 Python 中对协程的支持。

本规范假定你了解 Python 中协程的实现(PEP 342PEP 380)。这里提出的语法更改的动机来自 asyncio 框架(PEP 3156)和“协函数”提案(PEP 3152,现在已被本规范拒绝)。

从本文档的这一部分开始,我们将使用原生协程来指代使用新语法声明的函数。在必要时,将使用基于生成器的协程来指代基于生成器语法的协程。在两种定义都适用的情况下,使用协程

新的协程声明语法

以下新语法用于声明原生协程

async def read_data(db):
    pass

协程的关键属性

  • async def 函数始终是协程,即使它们不包含 await 表达式。
  • async 函数中使用 yieldyield from 表达式将导致 SyntaxError
  • 在内部,引入了两个新的代码对象标志:
    • CO_COROUTINE 用于标记原生协程(使用新语法定义)。
    • CO_ITERABLE_COROUTINE 用于使基于生成器的协程原生协程兼容(由 types.coroutine() 函数设置)。
  • 普通生成器在被调用时,返回一个生成器对象;类似地,协程返回一个协程对象。
  • StopIteration 异常不会从协程中传播出去,而是被替换为 RuntimeError。对于普通生成器,这种行为需要进行未来导入(请参阅 PEP 479)。
  • 原生协程被垃圾回收时,如果它从未被等待过,将引发 RuntimeWarning(另请参阅 调试功能 部分)。
  • 另请参阅 协程对象 部分。

types.coroutine()

新的函数 coroutine(fn) 被添加到 types 模块中。它允许 asyncio 中现有的基于生成器的协程与本 PEP 中引入的原生协程之间相互操作。

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

该函数将 CO_ITERABLE_COROUTINE 标志应用于生成器函数的代码对象,使其返回一个协程对象。

如果 fn 不是生成器函数,它将被包装。如果它返回一个生成器,它将被包装在一个可等待代理对象中(请参阅下面关于可等待对象的定义)。

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

Await 表达式

以下新的 await 表达式用于获取协程执行的结果

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

awaityield from 类似,会挂起 read_data 协程的执行,直到 db.fetch等待对象完成并返回结果数据。

它使用 yield from 实现,并额外增加了一个验证其参数的步骤。 await 仅接受一个 *可等待对象*,它可以是以下之一

  • 由 *原生协程函数* 返回的 *原生协程* 对象。
  • 由用 types.coroutine() 装饰的函数返回的 *基于生成器的协程* 对象。
  • 具有 __await__ 方法的对象,该方法返回一个迭代器。

    任何 yield from 调用链都以 yield 结尾。 这是 *Futures* 实现的根本机制。 由于协程在内部是特殊类型的生成器,因此每个 await 都通过 yieldawait 调用链中的某个位置被挂起(有关详细解释,请参考 PEP 3156)。

    为了使协程能够实现此行为,添加了一个名为 __await__ 的新魔术方法。 例如,在 asyncio 中,为了使 await 语句中的 *Future* 对象能够使用,唯一的更改是在 asyncio.Future 类中添加 __await__ = __iter__ 行。

    具有 __await__ 方法的对象在本 PEP 的其余部分中称为 *类似 Future* 对象。

    如果 __await__ 返回的不是迭代器,则会引发 TypeError

  • 使用 CPython C API 定义的具有 tp_as_async.am_await 函数的对象,该函数返回一个 *迭代器*(类似于 __await__ 方法)。

async def 函数之外使用 await 会引发 SyntaxError(就像在 def 函数之外使用 yield 会引发 SyntaxError 一样)。

将除 *可等待对象* 之外的任何对象传递给 await 表达式会引发 TypeError

更新的操作符优先级表

await 关键字的定义如下

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

其中“primary”代表语言中最紧密绑定的操作。 它的语法是

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

有关详细信息,请参阅 Python 文档 [12] 和本提案的 语法更新 部分。

awaityieldyield from 运算符的主要区别在于 *await 表达式* 通常不需要在其周围使用括号。

此外,yield from 允许任何表达式作为其参数,包括像 yield from a() + b() 这样的表达式,它们会被解析为 yield from (a() + b()),而这几乎总是错误。 通常,任何算术运算的结果都不是 *可等待对象*。 为了避免此类错误,决定将 await 的优先级设置为低于 []().,但高于 ** 运算符。

运算符 描述
yield xyield from x yield 表达式
lambda lambda 表达式
ifelse 条件表达式
or 布尔或
and 布尔与
not x 布尔非
innot inisis 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
pair = 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* 方法中调用异步代码。 为了支持异步迭代

  1. 一个对象必须实现一个 __aiter__ 方法(或者,如果使用 CPython C API 定义,则为 tp_as_async.am_aiter 槽),该方法返回一个 *异步迭代器对象*。
  2. *异步迭代器对象* 必须实现一个 __anext__ 方法(或者,如果使用 CPython C API 定义,则为 tp_as_async.am_anext 槽),该方法返回一个 *可等待对象*。
  3. 为了停止迭代,__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 会导致 SyntaxError

与常规 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'

and

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 中现有的基于生成器的协程的行为保持不变。

已经做出了很大的努力来确保协程和生成器被视为不同的概念。

  1. 原生协程对象不实现 __iter____next__ 方法。因此,它们不能被迭代或传递给 iter()list()tuple() 和其他内置函数。它们也不能在 for..in 循环中使用。

    尝试在原生协程对象上使用 __iter____next__ 将导致 TypeError

  2. 普通生成器不能 yield from 原生协程:这样做会导致 TypeError
  3. 基于生成器的协程(用于 asyncio 代码必须用 @asyncio.coroutine [1] 装饰)可以 yield from 原生协程对象
  4. inspect.isgenerator()inspect.isgeneratorfunction()原生协程对象和原生协程函数返回 False

协程对象方法

协程在内部基于生成器,因此它们共享实现。与生成器对象类似,协程具有 throw()send()close() 方法。 StopIterationGeneratorExit 对协程的作用相同(尽管 PEP 479 默认情况下已启用用于协程)。有关详细信息,请参见 PEP 342PEP 380 和 Python 文档 [11]

throw()send() 方法用于协程,用于将值和错误推送到类 Future 的对象中。

调试功能

初学者常犯的一个错误是忘记在协程上使用 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_wrapperget_coroutine_wrapper。 这是为了在 asyncio 和其他框架中启用高级调试功能(例如,显示协程创建的位置以及协程被垃圾回收的更详细的堆栈跟踪)。

新的标准库函数

  • types.coroutine(gen)。有关详细信息,请参见 types.coroutine() 部分。
  • inspect.iscoroutine(obj) 如果 obj 是一个原生协程对象,则返回 True
  • inspect.iscoroutinefunction(obj) 如果 obj 是一个原生协程函数,则返回 True
  • inspect.isawaitable(obj) 如果 obj 是一个可等待对象,则返回 True
  • inspect.getcoroutinestate(coro) 返回原生协程对象的当前状态(反映 inspect.getfgeneratorstate(gen))。
  • inspect.getcoroutinelocals(coro) 返回原生协程对象的局部变量与其值的映射(反映 inspect.getgeneratorlocals(gen))。
  • sys.set_coroutine_wrapper(wrapper) 允许拦截原生协程对象的创建。 wrapper 必须是一个可调用对象,它接受一个参数(一个协程对象),或者 NoneNone 会重置包装器。如果调用两次,新的包装器将替换以前的包装器。该函数是特定于线程的。有关详细信息,请参见 调试功能
  • sys.get_coroutine_wrapper() 返回当前的包装器对象。如果未设置任何包装器,则返回 None。该函数是特定于线程的。有关详细信息,请参见 调试功能

新的抽象基类

为了更好地与现有框架(如 Tornado,参见 [13])和编译器(如 Cython,参见 [16])集成,添加了两个新的抽象基类 (ABC)

  • collections.abc.Awaitable ABC 用于类 Future 的类,这些类实现了 __await__ 方法。
  • collections.abc.Coroutine ABC 用于协程对象,这些对象实现了 send(value)throw(type, exc, tb)close()__await__() 方法。

    请注意,带有 CO_ITERABLE_COROUTINE 标志的基于生成器的协程不实现 __await__ 方法,因此不是 collections.abc.Coroutinecollections.abc.Awaitable ABC 的实例。

    @types.coroutine
    def gencoro():
        yield
    
    assert not isinstance(gencoro(), collections.abc.Coroutine)
    
    # however:
    assert inspect.isawaitable(gencoro())
    

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

  • collections.abc.AsyncIterable – 测试 __aiter__ 方法。
  • collections.abc.AsyncIterator – 测试 __aiter____anext__ 方法。

术语表

原生协程函数
协程函数用 async def 声明。它使用 awaitreturn value;有关详细信息,请参见 新的协程声明语法
原生协程
从原生协程函数返回。有关详细信息,请参见 Await 表达式
基于生成器的协程函数
基于生成器语法的协程。最常见的例子是使用 @asyncio.coroutine 装饰的函数。
基于生成器的协程
从基于生成器的协程函数返回。
协程
原生协程基于生成器的协程
协程对象
原生协程 对象或 基于生成器的协程 对象。
类似 Future 的对象
具有 __await__ 方法的对象,或具有 tp_as_async->am_await 函数的 C 对象,返回一个 迭代器。可以在协程中通过 await 表达式进行使用。协程等待类似 Future 的对象会挂起,直到类似 Future 的对象的 __await__ 完成并返回结果。有关详细信息,请参阅 Await 表达式
可等待对象
一个 类似 Future 的 对象或一个 协程 对象。有关详细信息,请参阅 Await 表达式
异步上下文管理器
异步上下文管理器具有 __aenter____aexit__ 方法,可以使用 async with。有关详细信息,请参阅 异步上下文管理器和“async with”
异步可迭代对象
具有 __aiter__ 方法的对象,该方法必须返回一个 异步迭代器 对象。可以使用 async for。有关详细信息,请参阅 异步迭代器和“async for”
异步迭代器
异步迭代器具有 __anext__ 方法。有关详细信息,请参阅 异步迭代器和“async for”

迁移计划

为了避免与 asyncawait 关键字的向后兼容性问题,决定修改 tokenizer.c,使其

  • 识别 async def NAME 标记组合;
  • 在对 async def 块进行标记时,用 ASYNC 替换 'async' NAME 标记,用 AWAIT 替换 'await' NAME 标记;
  • 在对 def 块进行标记时,按原样生成 'async''await' NAME 标记。

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

在同一代码片段中使用“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%,即所有现有代码将按原样运行。

所需更改主要是

  1. 修改 @asyncio.coroutine 装饰器,以使用新的 types.coroutine() 函数。
  2. asyncio.Future 类中添加 __await__ = __iter__ 行。
  3. 添加 ensure_future() 作为 async() 函数的别名。弃用 async() 函数。

asyncio 迁移策略

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

CPython 代码库中的 async/await

在 CPython 中没有使用 await 名称。

async 主要由 asyncio 使用。我们通过将 async() 函数重命名为 ensure_future() 来解决此问题(有关详细信息,请参阅 asyncio 部分)。

async 关键字的另一种用法是在 Lib/xml/dom/xmlbuilder.py 中,为 DocumentLS 类定义 async = False 属性。对此没有文档或测试,它在 CPython 中的任何其他地方都没有使用。它被替换为一个 getter,该 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*

弃用计划

asyncawait 名称将在 CPython 3.5 和 3.6 中被软弃用。在 3.7 中,我们将它们转换为适当的关键字。在 3.7 之前将 asyncawait 转换为适当的关键字可能会使人们更难将他们的代码移植到 Python 3。

设计考虑

PEP 3152

PEP 3152 由 Gregory Ewing 提议,为协程提供了一种不同的机制(称为“cofunctions”)。一些关键点

  1. 一个新的关键字 codef 来声明一个 cofunctionCofunction 始终是一个生成器,即使其中没有 cocall 表达式。映射到本提案中的 async def
  2. 一个新的关键字 cocall 来调用一个 cofunction。只能在 cofunction 中使用。映射到本提案中的 await(有一些区别,请见下文)。
  3. 没有 cocall 关键字,就无法调用 cofunction
  4. cocall 在语法上要求在其后面使用圆括号
    atom: cocall | <existing alternatives for atom>
    cocall: 'cocall' atom cotrailer* '(' [arglist] ')'
    cotrailer: '[' subscriptlist ']' | '.' NAME
    
  5. cocall f(*args, **kwds) 在语义上等效于 yield from f.__cocall__(*args, **kwds)

与本提案的区别

  1. 在本 PEP 中没有与 __cocall__ 等效的项,该项被调用,其结果被传递给 yield fromcocall 表达式中。 await 关键字期望一个 可等待的 对象,验证类型,并在其上执行 yield from。尽管 __await__ 方法类似于 __cocall__,但它仅用于定义 类似 Future 的 对象。
  2. await 的定义与语法中的 yield from 几乎相同(稍后强制执行 await 只能在 async def 内部使用)。可以简单地编写 await future,而 cocall 始终需要圆括号。
  3. 要使 asyncio 与 PEP 3152 一起使用,需要修改 @asyncio.coroutine 装饰器,以将所有函数包装在具有 __cocall__ 方法的对象中,或者在生成器上实现 __cocall__。要从现有的基于生成器的协程调用 cofunctions,需要使用 costart(cofunc, *args, **kwargs) 内置函数。
  4. 由于没有 cocall 关键字就无法调用 cofunction,因此它会自动防止忘记在基于生成器的协程上使用 yield from 的常见错误。本提案以不同的方法解决了这个问题,请参阅 调试功能
  5. 要求使用 cocall 关键字来调用协程的一个缺点是,如果决定实现协程生成器——具有 yieldasync yield 表达式的协程——我们就不需要使用 cocall 关键字来调用它们。因此,我们最终将为常规协程使用 __cocall__ 而没有 __call__,而为协程生成器使用 __call__ 而没有 __cocall__
  6. 在语法上要求使用圆括号也会引入很多新的问题。

    以下代码

    await fut
    await function_returning_future()
    await asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))
    

    看起来像

    cocall fut()  # or cocall costart(fut)
    cocall (function_returning_future())()
    cocall asyncio.gather(costart(coro1, arg1, arg2),
                          costart(coro2, arg1, arg2))
    
  7. PEP 3152 中没有与 async forasync with 等效的项。

协程-生成器

使用 async for 关键字,需要一个协程生成器的概念 - 一个包含 yieldyield from 表达式的协程。为了避免与普通生成器的歧义,我们可能需要在 yield 之前添加一个 async 关键字,而 async yield from 将会抛出一个 StopAsyncIteration 异常。

虽然可以实现协程生成器,但我们认为它超出了本提案的范围。这是一个高级概念,需要谨慎考虑和平衡,对当前生成器对象的实现进行非平凡的更改。这需要一个单独的 PEP 来处理。

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

async/await 在编程语言中不是一个新概念

  • C# 很早就有了它 [5];
  • 在 ECMAScript 7 中添加 async/await 的提案 [2]; 同时参见 Traceur 项目 [9];
  • Facebook 的 Hack/HHVM [6];
  • Google 的 Dart 语言 [7];
  • Scala [8];
  • 在 C++ 中添加 async/await 的提案 [10];
  • 以及许多其他不太流行的语言。

这是一个巨大的优势,因为一些用户已经熟悉 async/await,并且它使在一个项目中使用多种语言变得更容易(例如 Python 与 ECMAScript 7)。

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

PEP 492 在 CPython 3.5.0 中被接受,其中定义了 __aiter__ 作为一种方法,该方法预期返回一个可等待的对象,并解析为一个异步迭代器。

在 3.5.2(由于 PEP 492 是以试用方式被接受的)中,__aiter__ 协议被更新为直接返回异步迭代器。

这一更改背后的动机是使在 Python 中实现异步生成器成为可能。有关更多详细信息,请参阅 [19][20]

“async”关键字的重要性

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

假设 Python 只拥有 await 关键字

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

def important():
    await useful()

如果 useful() 函数被重构,有人从中删除了所有 await 表达式,它将成为一个普通的 Python 函数,所有依赖它的代码,包括 important(),将被破坏。为了缓解这个问题,必须引入一个类似于 @asyncio.coroutine 的装饰器。

为什么使用“async def”

对于一些人来说,裸 async name(): pass 语法可能比 async def name(): pass 更吸引人。它肯定更容易输入。但另一方面,它打破了 async defasync withasync for 之间的对称性,其中 async 是一个修饰符,表示语句是异步的。它也与现有的语法更一致。

为什么不使用“await for”和“await with”

async 是一个形容词,因此它更适合作为语句限定符关键字。 await for/with 将意味着某些东西正在等待 forwith 语句的完成。

为什么使用“async def”,而不是“def async”

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

在主语句关键字之后添加“async”可能会引起一些混淆,例如“for async item in iterator”可以理解为“迭代器中每个异步项”。

defwithfor 之前添加 async 关键字也会使语言语法更简单。而“async def”在视觉上更好地将协程与普通函数区分开来。

为什么不使用 __future__ 导入

过渡计划 部分解释了如何修改词法分析器,以便在 async def 块中asyncawait 视为关键字。因此,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 语句中使用的对象;
  • 它将破坏向后兼容性,因为在 Python <= 3.4 中,没有任何东西可以阻止从 __enter__ 和/或 __exit__ 返回类 Future 的对象;
  • 本提案的主要目的之一是使原生协程尽可能简单可靠,因此协议之间需要明确分离。

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

现有基于生成器的协程和本提案背后的愿景是,让用户能够轻松地看到代码可能被挂起的位置。使现有的“for”和“with”语句识别异步迭代器和上下文管理器将不可避免地创建隐式挂起点,从而使代码推理变得更加困难。

推导式

可以提供用于异步推导的语法,但这种结构超出了本 PEP 的范围。

异步 lambda 函数

可以提供用于异步 lambda 函数的语法,但这种结构超出了本 PEP 的范围。

性能

总体影响

本提案没有引入任何可观察到的性能影响。以下是 Python 官方基准测试集的输出 [4]

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 次调用。

参考实现

参考实现可以在这里找到:[3].

高级更改和新协议列表

  1. 用于定义协程的新语法:async def 和新的 await 关键字。
  2. 用于类 Future 对象的新 __await__ 方法,以及 PyTypeObject 中的新 tp_as_async.am_await 槽位。
  3. 用于异步上下文管理器的新的语法:async with。以及与之相关的协议,其中包含 __aenter____aexit__ 方法。
  4. 用于异步迭代的新语法:async for。以及与之相关的协议,其中包含 __aiter____aexit__ 和新的内置异常 StopAsyncIterationPyTypeObject 中的新 tp_as_async.am_aitertp_as_async.am_anext 槽位。
  5. 新的 AST 节点:AsyncFunctionDefAsyncForAsyncWithAwait
  6. 新函数:sys.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)
  7. 用于代码对象的新的 CO_COROUTINECO_ITERABLE_COROUTINE 位标志。
  8. 新的 ABC:collections.abc.Awaitablecollections.abc.Coroutinecollections.abc.AsyncIterablecollections.abc.AsyncIterator
  9. C API 更改:新的 PyCoro_Type(在 Python 中暴露为 types.CoroutineType)和 PyCoroObjectPyCoro_CheckExact(*o) 用于测试 o 是否为原生协程

虽然更改和新增功能的列表不短,但重要的是要理解,大多数用户不会直接使用这些功能。它旨在用于框架和库中,为用户提供方便使用且明确的 API,使用 async defawaitasync forasync with 语法。

工作示例

本 PEP 中提出的所有概念都已实现 [3],并且可以进行测试。

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()

接受

PEP 492 于 2015 年 5 月 5 日星期二被 Guido 接受 [14]

实现

该实现是在问题 24017 中跟踪的 [15]。它于 2015 年 5 月 11 日提交。

参考资料

致谢

感谢 Guido van Rossum、Victor Stinner、Elvis Pranskevichus、Andrew Svetlov、Łukasz Langa、Greg Ewing、Stephen J. Turnbull、Jim J. Jewett、Brett Cannon、Alyssa Coghlan、Steven D’Aprano、Paul Moore、Nathaniel Smith、Ethan Furman、Stefan Behnel、Paul Sokolovsky、Victor Petrovykh 以及其他许多人对本 PEP 的反馈、想法、编辑、批评、代码审查和讨论。


来源:https://github.com/python/peps/blob/main/peps/pep-0492.rst

上次修改时间:2023-10-11 12:05:51 GMT