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 初始测试版的反馈导致支持本 PEP 的对象模型重新设计,以便更清晰地将原生协程与生成器区分开来——原生协程现在是它们自己完全不同的类型(在 [17] 中实现),而不再是一种新型生成器。

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

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

    在 3.5.2 之前,__aiter__ 预期返回一个 *awaitable*,它解析为一个 *异步迭代器*。从 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 语言的原生功能,并将其与生成器明确分离。这消除了生成器/协程的歧义,并使得无需依赖特定库即可可靠地定义协程。这也使 linters 和 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 替换。对于普通生成器,这种行为需要 future 导入(参见 PEP 479)。
  • 当一个 *原生协程* 被垃圾回收时,如果它从未被 await 过,将引发 RuntimeWarning(另请参阅 调试功能)。
  • 另请参阅 协程对象 部分。

types.coroutine()

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

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

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

如果 fn 不是 *生成器函数*,它将被包装。如果它返回一个 *生成器*,它将被包装在一个 *awaitable* 代理对象中(参见下面 awaitable 对象的定义)。

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

Await 表达式

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

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

await,与 yield from 类似,会暂停 read_data 协程的执行,直到 db.fetch *awaitable* 完成并返回结果数据。

它使用 yield from 的实现,并额外增加了对其参数的验证。await 只接受一个 *awaitable*,它可以是以下之一:

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

    任何 yield from 调用链都以一个 yield 结束。这是 *Futures* 实现的基本机制。由于协程在内部是特殊类型的生成器,每个 await 都会被 await 调用链中某个地方的 yield 暂停(请参阅 PEP 3156 以获取详细解释)。

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

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

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

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

async def 函数之外使用 awaitSyntaxError(就像在 def 函数之外使用 yieldSyntaxError 一样)。

将非 *awaitable* 对象传递给 await 表达式会引发 TypeError

更新的操作符优先级表

await 关键字定义如下

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

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

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

更多详情请参阅 Python 文档 [12] 和本提案的 语法更新 部分。

yieldyield from 操作符相比,await 的主要区别在于 *await 表达式* 大多数情况下不需要括号。

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

操作符 描述
yield x, yield from x yield 表达式
lambda Lambda 表达式
ifelse 条件表达式
布尔或
布尔与
not x 布尔非
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
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”

一个 *异步上下文管理器* 是一个上下文管理器,它能够在其 *进入* 和 *退出* 方法中暂停执行。

为了实现这一点,提出了一个用于异步上下文管理器的新协议。添加了两个新的魔术方法:__aenter____aexit__。两者都必须返回一个 *awaitable*。

异步上下文管理器的示例

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 withSyntaxError

示例

通过 *异步上下文管理器*,可以轻松为协程实现正确的数据库事务管理器

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 槽),返回一个 *awaitable*。
  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 forSyntaxError

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

我们付出了巨大的努力,以确保协程和生成器被视为不同的概念

  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 装饰器的行为没有影响。

本提案提出,协程是与生成器不同的原生概念。*除了* 在从未被 await 的协程上引发 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 是 *awaitable*,则返回 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 抽象基类,用于实现 __await__ 方法的 *Future-like* 类。
  • collections.abc.Coroutine 抽象基类,用于实现 send(value)throw(type, exc, tb)close()__await__() 方法的 *协程* 对象。

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

    @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 表达式
Awaitable
一个 *Future-like* 对象或一个 *协程* 对象。详情请参阅 Await 表达式
异步上下文管理器
异步上下文管理器具有 __aenter____aexit__ 方法,并且可以与 async with 一起使用。详情请参阅 异步上下文管理器和“async with”
异步可迭代对象
一个带有 __aiter__ 方法的对象,该方法必须返回一个 *异步迭代器* 对象。可以与 async for 一起使用。详情请参阅 异步迭代器和“async for”
异步迭代器
异步迭代器有一个 __anext__ 方法。详情请参阅 异步迭代器和“async for”

过渡计划

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

  • 识别 async def NAME 令牌组合;
  • 在词法分析 async def 块时,它将 'async' NAME 令牌替换为 ASYNC,将 'await' NAME 令牌替换为 AWAIT
  • 在词法分析 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

Gregory Ewing 的 PEP 3152 提出了另一种协程机制(称为“协函数”)。一些关键点

  1. 一个新的关键字 codef 用于声明一个 *协函数*。*协函数* 总是生成器,即使它内部没有 cocall 表达式。在本提案中映射到 async def
  2. 一个新的关键字 cocall 用于调用 *协函数*。只能在 *协函数* 内部使用。在本提案中映射到 await(有一些差异,见下文)。
  3. 没有 cocall 关键字,不可能调用 *协函数*。
  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__ 的等价物,它被调用并且其结果在 cocall 表达式中传递给 yield fromawait 关键字期望一个 *awaitable* 对象,验证类型,并在其上执行 yield from。尽管 __await__ 方法与 __cocall__ 相似,但它仅用于定义 *类 Future* 对象。
  2. await 在语法上与 yield from 的定义几乎相同(后来强制规定 await 只能在 async def 内部)。可以直接写 await future,而 cocall 总是需要括号。
  3. 为了使 asyncio 与 PEP 3152 一起工作,需要修改 @asyncio.coroutine 装饰器,将所有函数包装在一个带有 __cocall__ 方法的对象中,或者在生成器上实现 __cocall__。要从现有基于生成器的协程调用 *协函数*,需要使用内置函数 costart(cofunc, *args, **kwargs)
  4. 由于没有 cocall 关键字就无法调用 *协函数*,这自动防止了忘记对基于生成器的协程使用 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__”不返回一个 awaitable

PEP 492 在 CPython 3.5.0 中被接受,其中 __aiter__ 被定义为一个方法,预期返回一个解析为异步迭代器的 awaitable。

在 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”可能被解读为“对于迭代器中的每个异步项”。

async 关键字放在 defwithfor 之前也使语言语法更简单。而且“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__
        ...

这种方法有以下缺点

  • 不可能创建一个同时在 withasync 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__ 和新的内置异常 StopAsyncIteration 相关的协议。PyTypeObject 中的新 tp_as_async.am_aitertp_as_async.am_anext 槽。
  5. 新的 AST 节点:AsyncFunctionDef, AsyncFor, AsyncWith, Await
  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. 新的抽象基类: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 已由 Guido 于 2015 年 5 月 5 日星期二接受 [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

最后修改:2025-02-01 08:55:40 GMT