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 设计和实现修订
- 对 Python 3.5 初始 beta 版本的反馈导致了支持本 PEP 的对象模型的重新设计,以便更清楚地将原生协程与生成器分离——原生协程不再是生成器的新类型,而是它们自己完全不同的类型(在 [17] 中实现)。
此更改主要基于尝试将原生协程支持集成到 Tornado Web 服务器时遇到的问题(在 [18] 中报告)。
- 在 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
。
基本原理和目标
当前 Python 支持使用生成器(PEP 342)实现协程,并通过 PEP 380 中引入的 yield from
语法进一步增强。这种方法有一些缺点:
- 协程很容易与普通生成器混淆,因为它们共享相同的语法;这对新手开发者来说尤其如此。
- 函数是否是协程由其主体中是否存在
yield
或yield from
语句决定,这会导致在重构期间此类语句出现在函数主体或从函数主体中消失时出现难以察觉的错误。 - 异步调用的支持仅限于
yield
在语法上允许的表达式,这限制了语法特征(如with
和for
语句)的实用性。
本提案将协程作为 Python 语言的原生功能,并将其与生成器明确区分开来。这消除了生成器/协程的歧义,并使得能够在不依赖于特定库的情况下可靠地定义协程。这也使 linter 和 IDE 能够改进静态代码分析和重构。
原生协程和相关的新语法功能使得能够以异步方式定义上下文管理器和迭代协议。如本提案后面所示,新的 async with
语句使 Python 程序能够在进入和退出运行时上下文时执行异步调用,而新的 async for
语句使得能够在迭代器中执行异步调用。
规范
本提案引入了新的语法和语义,以增强 Python 中对协程的支持。
本规范假定你了解 Python 中协程的实现(PEP 342 和 PEP 380)。这里提出的语法更改的动机来自 asyncio 框架(PEP 3156)和“协函数”提案(PEP 3152,现在已被本规范拒绝)。
从本文档的这一部分开始,我们将使用原生协程来指代使用新语法声明的函数。在必要时,将使用基于生成器的协程来指代基于生成器语法的协程。在两种定义都适用的情况下,使用协程。
新的协程声明语法
以下新语法用于声明原生协程
async def read_data(db):
pass
协程的关键属性
async def
函数始终是协程,即使它们不包含await
表达式。- 在
async
函数中使用yield
或yield 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 ...')
...
await
与 yield from
类似,会挂起 read_data
协程的执行,直到 db.fetch
可等待对象完成并返回结果数据。
它使用 yield from
实现,并额外增加了一个验证其参数的步骤。 await
仅接受一个 *可等待对象*,它可以是以下之一
- 由 *原生协程函数* 返回的 *原生协程* 对象。
- 由用
types.coroutine()
装饰的函数返回的 *基于生成器的协程* 对象。 - 具有
__await__
方法的对象,该方法返回一个迭代器。任何
yield from
调用链都以yield
结尾。 这是 *Futures* 实现的根本机制。 由于协程在内部是特殊类型的生成器,因此每个await
都通过yield
在await
调用链中的某个位置被挂起(有关详细解释,请参考 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] 和本提案的 语法更新 部分。
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 |
布尔或 |
and |
布尔与 |
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”
*异步上下文管理器* 是一种上下文管理器,它能够在其 *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* 方法中调用异步代码。 为了支持异步迭代
- 一个对象必须实现一个
__aiter__
方法(或者,如果使用 CPython C API 定义,则为tp_as_async.am_aiter
槽),该方法返回一个 *异步迭代器对象*。 - *异步迭代器对象* 必须实现一个
__anext__
方法(或者,如果使用 CPython C API 定义,则为tp_as_async.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
会导致 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 中现有的基于生成器的协程的行为保持不变。
已经做出了很大的努力来确保协程和生成器被视为不同的概念。
- 原生协程对象不实现
__iter__
和__next__
方法。因此,它们不能被迭代或传递给iter()
、list()
、tuple()
和其他内置函数。它们也不能在for..in
循环中使用。尝试在原生协程对象上使用
__iter__
或__next__
将导致TypeError
。 - 普通生成器不能
yield from
原生协程:这样做会导致TypeError
。 - 基于生成器的协程(用于 asyncio 代码必须用
@asyncio.coroutine
[1] 装饰)可以yield from
原生协程对象。 inspect.isgenerator()
和inspect.isgeneratorfunction()
对原生协程对象和原生协程函数返回False
。
协程对象方法
协程在内部基于生成器,因此它们共享实现。与生成器对象类似,协程具有 throw()
、send()
和 close()
方法。 StopIteration
和 GeneratorExit
对协程的作用相同(尽管 PEP 479 默认情况下已启用用于协程)。有关详细信息,请参见 PEP 342、PEP 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_wrapper
和 get_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
必须是一个可调用对象,它接受一个参数(一个协程对象),或者None
。None
会重置包装器。如果调用两次,新的包装器将替换以前的包装器。该函数是特定于线程的。有关详细信息,请参见 调试功能。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.Coroutine
和collections.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
声明。它使用await
和return 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”。
迁移计划
为了避免与 async
和 await
关键字的向后兼容性问题,决定修改 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%,即所有现有代码将按原样运行。
所需更改主要是
- 修改
@asyncio.coroutine
装饰器,以使用新的types.coroutine()
函数。 - 在
asyncio.Future
类中添加__await__ = __iter__
行。 - 添加
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*
弃用计划
async
和 await
名称将在 CPython 3.5 和 3.6 中被软弃用。在 3.7 中,我们将它们转换为适当的关键字。在 3.7 之前将 async
和 await
转换为适当的关键字可能会使人们更难将他们的代码移植到 Python 3。
设计考虑
PEP 3152
PEP 3152 由 Gregory Ewing 提议,为协程提供了一种不同的机制(称为“cofunctions”)。一些关键点
- 一个新的关键字
codef
来声明一个 cofunction。Cofunction 始终是一个生成器,即使其中没有cocall
表达式。映射到本提案中的async def
。 - 一个新的关键字
cocall
来调用一个 cofunction。只能在 cofunction 中使用。映射到本提案中的await
(有一些区别,请见下文)。 - 没有
cocall
关键字,就无法调用 cofunction。 cocall
在语法上要求在其后面使用圆括号atom: cocall | <existing alternatives for atom> cocall: 'cocall' atom cotrailer* '(' [arglist] ')' cotrailer: '[' subscriptlist ']' | '.' NAME
cocall f(*args, **kwds)
在语义上等效于yield from f.__cocall__(*args, **kwds)
。
与本提案的区别
- 在本 PEP 中没有与
__cocall__
等效的项,该项被调用,其结果被传递给yield from
在cocall
表达式中。await
关键字期望一个 可等待的 对象,验证类型,并在其上执行yield from
。尽管__await__
方法类似于__cocall__
,但它仅用于定义 类似 Future 的 对象。 await
的定义与语法中的yield from
几乎相同(稍后强制执行await
只能在async def
内部使用)。可以简单地编写await future
,而cocall
始终需要圆括号。- 要使 asyncio 与 PEP 3152 一起使用,需要修改
@asyncio.coroutine
装饰器,以将所有函数包装在具有__cocall__
方法的对象中,或者在生成器上实现__cocall__
。要从现有的基于生成器的协程调用 cofunctions,需要使用costart(cofunc, *args, **kwargs)
内置函数。 - 由于没有
cocall
关键字就无法调用 cofunction,因此它会自动防止忘记在基于生成器的协程上使用yield from
的常见错误。本提案以不同的方法解决了这个问题,请参阅 调试功能。 - 要求使用
cocall
关键字来调用协程的一个缺点是,如果决定实现协程生成器——具有yield
或async yield
表达式的协程——我们就不需要使用cocall
关键字来调用它们。因此,我们最终将为常规协程使用__cocall__
而没有__call__
,而为协程生成器使用__call__
而没有__cocall__
。 - 在语法上要求使用圆括号也会引入很多新的问题。
以下代码
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))
- PEP 3152 中没有与
async for
和async with
等效的项。
协程-生成器
使用 async for
关键字,需要一个协程生成器的概念 - 一个包含 yield
和 yield 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__
协议被更新为直接返回异步迭代器。
“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 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”可以理解为“迭代器中每个异步项”。
在 def
、with
和 for
之前添加 async
关键字也会使语言语法更简单。而“async def”在视觉上更好地将协程与普通函数区分开来。
为什么不使用 __future__ 导入
过渡计划 部分解释了如何修改词法分析器,以便在 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
语句中使用的对象; - 它将破坏向后兼容性,因为在 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].
高级更改和新协议列表
- 用于定义协程的新语法:
async def
和新的await
关键字。 - 用于类 Future 对象的新
__await__
方法,以及PyTypeObject
中的新tp_as_async.am_await
槽位。 - 用于异步上下文管理器的新的语法:
async with
。以及与之相关的协议,其中包含__aenter__
和__aexit__
方法。 - 用于异步迭代的新语法:
async for
。以及与之相关的协议,其中包含__aiter__
、__aexit__
和新的内置异常StopAsyncIteration
。PyTypeObject
中的新tp_as_async.am_aiter
和tp_as_async.am_anext
槽位。 - 新的 AST 节点:
AsyncFunctionDef
、AsyncFor
、AsyncWith
、Await
。 - 新函数:
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)
。 - 用于代码对象的新的
CO_COROUTINE
和CO_ITERABLE_COROUTINE
位标志。 - 新的 ABC:
collections.abc.Awaitable
、collections.abc.Coroutine
、collections.abc.AsyncIterable
和collections.abc.AsyncIterator
。 - C API 更改:新的
PyCoro_Type
(在 Python 中暴露为types.CoroutineType
)和PyCoroObject
。PyCoro_CheckExact(*o)
用于测试o
是否为原生协程。
虽然更改和新增功能的列表不短,但重要的是要理解,大多数用户不会直接使用这些功能。它旨在用于框架和库中,为用户提供方便使用且明确的 API,使用 async def
、await
、async for
和 async 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()
接受
实现
该实现是在问题 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