前言
本文的主体内容大部分来自对 PEP 492 原文的翻译,剩余部分是本人对原文的理解,在整理过程中我没有刻意地区分二者,这两部分被糅杂在一起形成了本文。因此请不要带着「本文的内容是百分百正确」的想法阅读。如果文中的某些内容让你产生疑惑,你可以给我留言与我讨论或者对比 PEP 492 的原文加以确认。
注:PEP 492 创建于 2015-04-09,Python 3.5
注:文中的「当前版本」指的是本提案生效之前的版本
注:本文过长,虽然已经自我校对过一次,但还是难免存在错别字或语句不通顺的地方,如果您发现了问题欢迎留言给我
摘要
网络请求爆发性地增长引发了对低延时、可拓展代码的相关需求。本提案旨在让显式地编写异步、并发 Python 代码更容易、更 Pythoinc,并以此满足前述需求。
提案建议使协程成为 Python 中完全独立的新概念,并引入新的支持语法。最终的目的是在 Python 中建立一个简洁通用的异步编程心智模型,并使它尽可能接近同步编程。
在本提案中,假设异步任务都使用类似内置模块 asyncio.events.AbstractEventLoop
中的事件循环进行编排和协调。但是,本提案与任何特定的事件循环实现无关,只与使用 yield
作为调度信号的协程相关,也就是说协程会在事件(例如 IO)完成前保持等待。
我们相信,本提案能够让 Python 在快速增长的异步编程领域中继续保持竞争力,因为很多其他语言已经或计划采用近似的特性:2,5,7,8,10。
接口设计与实施修订
注:这部分是修订内容,可以放在最后阅读。
- 以初始 Python 3.5 beta 版本的反馈为依据,重构本提案设置的对象模型。这次重构的目的是更明确地将原生协程与生成器分离,而不是将原生协程作为一种新的生成器,原生协程要设计成完全独立的类型(具体实施在 引用 17)。这么做的主要原因是在尝试为 Tornado Web Server 集成原生协程时遇到了问题(记录在 引用 18)。
- CPython 3.5.2 更新了
__aiter__
协议。在 3.5.2 之前,__aiter__
返回一个可以被解析成 异步迭代器 的 可等待对象(awaitable)。从 3.5.2 开始,__aiter__
直接返回异步迭代器。如果在 3.5.2 中使用旧协议,会抛出一个PendingDeprecatioWarning
异常。而在 3.6 中,旧的__aiter__
协议仍旧会被支持,但会抛出一个DeprecationWarning
异常。最终,在 3.7 中,旧的__aiter__
协议将被废弃,如果__aiter__
返回的不是异步迭代器,则会引发RuntimeError
。可以通过 引用 19 和 引用 20 来获取更多细节。
理由和目标
当前版本的 Python 支持通过生成器来实现协程(PEP 342),PEP 380 中引入的 yield from
语法进一步增强了这一特性。但是这种方案有很多缺点:
- 生成器实现的协程和正常生成器的语法相同,因此很容易被混淆,对于新用户来说尤其如此;
- 一个函数是否是协程取决于函数体中是否存在
yield
或yield from
语句。在重构这些函数时,如果删除或新增了yield
相关语句就可能会导致一些不明显的错误; - 只能在
yield
语法支持的地方进行异步调用,无法异步调用类似 with 或 for 这样的语句,限制了可用性。
本提案使协程成为 Python 语言的一种原生特性,并且清晰地将其与生成器区分开。这样做不仅消除了生成器与协程之间的歧义,还可以不依赖特定库直接定义协程。同时也提升了 linters 或 IDE 静态代码分析和重构的能力。
原生协程以及相关新语法使得在异步操作中定义上下文管理器和可迭代协议成为可能。稍后会在提案中提及:新的 async with
语句允许 Python 程序在进入或退出上下文上时执行异步调用,而新的 async for
语句可以在迭代器中执行异步调用。
规范
规范章节引入了新的语法和语义,以增强 Python 对协程的支持。
本规范假定阅读者已经了解此前 Python 中协程的实现( PEP 342 和 PEP 380)。本规范涉及的语法修改动机来自 asyncio 模块提案(PEP 3156)和 Cofunctions 提案(PEP 3152,现已被本规范否决)。
在后文中,将使用「原生协程」来指代使用新语法声明的协程,使用「生成器式协程」指代基于生成器语法的协程。
原生协程声明语法
原生协程声明语法如下:
代码语言:javascript复制async def read_data(db):
pass
它的主要特性有:
- 使用
async def
声明的函数一定是协程,即使内部不包含await
; - 在
async
函数中使用yield
或yield from
会引发SyntaxError
异常; - 在内部,引入了两个新的 code object flags:
CO_COROUTINE
:用于标记原生协程;CO_ITERABLE_COROUTINE
:使生成器式协程与原生协程兼容(由 types.coroutine 函数设置)。
- 常规生成器返回一个生成器对象,类似的,协程返回一个协程对象;
- 在协程中
StopIteration
会被RuntimeError
代替,对于常规生成器来说,这种行为会在后续过程中支持(详情请看 PEP 479); - 如果不使用
await
直接调用原生协程,当它被垃圾回收时会抛出一个RuntimeWarning
(点击 用于调试的特性 了解更多); - 更多特性请看:协程对象 章节。
types.coroutine()
types
模块中新增了一个名为 coroutine(fn)
的函数。它能够帮助「asyncio 中现有的生成器式协程」与「本提案引入的原生协程」实现相互兼容:
@types.coroutine
def process_data(db):
data = yield from read_data(db)
...
代码语言:javascript复制# CPython 3.10 Lib.types.py
def coroutine(func):
if not callable(func):
raise TypeError('types.coroutine() expects a callable')
if (
func.__class__ is FunctionType and
getattr(func, '__code__', None).__class__ is CodeType
):
co_flags = func.__code__.co_flags
# 0x20 == CO_GENERATOR 生成器标识
# 0x180 == CO_COROUTINE | CO_ITERABLE_COROUTINE 原生协程标识 or 生成式协程标识
# 0x100 == CO_ITERABLE_COROUTINE 生成式协程标识
# 如果确定传入函数是一个原生协程则直接返回
if co_flags & 0x180:
return func
# 如果传入函数是一个生成器函数,
# 则将 CO_ITERABLE_COROUTINE 标记附加在此函数上,随后返回
if co_flags & 0x20:
co = func.__code__
func.__code__ = co.replace(co_flags=co.co_flags | 0x100)
return func
# 下面的代码主要用于兼容「返回类似生成器对象」的函数
# 例如使用 Cython 编译的生成器
import functools
import _collections_abc
@functools.wraps(func)
def wrapped(*args, **kwargs):
coro = func(*args, **kwargs)
if (coro.__class__ is CoroutineType or
coro.__class__ is GeneratorType and coro.gi_code.co_flags & 0x100):
return coro
if (isinstance(coro, _collections_abc.Generator) and
not isinstance(coro, _collections_abc.Coroutine)):
return _GeneratorWrapper(coro)
return coro
return wrapped
如果 fn
是生成器函数, types.coroutine()
会在它的 code object 中添加 CO_ITERABLE_COROUTINE
标志,使其返回一个协程对象。
如果 fn
不是生成器函数,types.coroutine()
会对齐进行包装。如果 fn
返回一个生成器函数,返回的函数会被 _GeneratorWrapper
包装。
type.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
后只能跟一个 可等待对象(awaitable),可以是以下选项之一:
- 原生协程函数返回的原生协程对象;
- 被
types.coroutine()
装饰的函数中返回的生成式协程对象; - 一个拥有
__await__
方法的对象,且该方法需要返回一个迭代器; - 使用 CPython C API 定义的带有
tp_as_async.am_await
函数的对象,该函数返回一个迭代器(类似__await__
方法)。
关于第三点一些延伸内容:任何 yield from
调用链都会以 yield
收尾,这是 Futures 执行的必要条件。由于协程本质上是一个特殊的生成器,因此每个 await
都会被 await
调用链上的某个 yield
挂起(详情请参考 PEP 3156)。为了在协程上实现这种行为,一个名为 __await__
的新魔术方法被添加进来。例如,在 asyncio 中, 要想在 await
语句中使用 Future,唯一要做的就是在 asyncio.Future
类中添加 __await__ = __iter__
。后续章节中,称带有 __await__
方法的对象为类 Future 对象。如果 __await__
返回迭代器之外的东西,会抛出 TypeError
异常。
在原生协程外部使用 await
会抛出 SyntaxError
异常(就像在一般函数外调用 yield
一样)。
不在 await
关键字后使用可等待对象会抛出 TypeError
异常。
更新运算符优先级
await
关键字被定义为:
power ::= await ["**" u_expr]
await ::= ["await"] primary
其中「primary」代表语言中最主要的操作,其语法为:
代码语言:javascript复制primary ::= atom | attributerf | subscription | slicing | call
如需理解上述表达式含义可参考 引用 12 和 语法更新。
与 yield
和 yield from
不同,大多数情况下 await
表达式不需要被圆括号包裹。此外,yield from
允许将任何表达式作为参数,甚至可以 yield from a() b()
,它会被解析为 yield from (a() b())
。这看起来很反常识就像一个 BUG 一样,因为一般来说,算数操作不会产生可等待对象。为了避免此类问题在 await
表达式中再次出现,await
的优先级被设定为低于 []
**,** ()
**,** .
但高于 ``**。
具体优先级如下(从上到下,从低到高):
Operator | Description |
---|---|
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 表达式 |
xindex, xindex:index, x(args...), x.attribute | 索引、切片、调用、属性 |
(expressions...), expressions..., {key: value...}, {expressions...} | 元组生成器、列表生成器、字典生成器、集合生成器 |
使用 await 关键字的示例
有效调用:
Expression | Will be parsed as |
---|---|
if await fut: pass | if (await fut): pass |
if await fut 1: poass | 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()) |
无效调用:
Expression | Shoud be written as |
---|---|
await await coro() | await (await coro()) |
await -coro() | await (-coro()) |
异步上下文管理器与 async with
注:关于上下文管理器的内容可以参考:WeeklyPEP-2-PEP343-with 语句-overview
所谓异步上下文管理器,是一种能够在进入或退出上下文时调用异步代码的上下文管理器。为了实现它,本规范单独为异步上下文提出了一个新协议,此协议由两个新的魔术方法组成:__aenter__
和 __aexit__
。它们都必须返回一个可等待对象。
异步上下文管理器的示例:
代码语言:javascript复制class AsyncContextManager:
async def __aenter__(self):
await log("entering context")
async def __aexit__(self, exc_type, exc, tb):
await log("exiting context")
新语法
本规范规定的异步上下文管理器声明方式如下:
代码语言:javascript复制async with EXPR as VAR:
BLOCK
语义上等同于:
代码语言:javascript复制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
异常。
示例
通过异步上下文管理器可以很方便的在协程中实现数据库事务管理器:
代码语言:javascript复制async def commit(session, data):
...
async with session.transaction():
...
await session.update(data)
...
也可以很简洁的使用锁:
代码语言:javascript复制async with lock:
...
代替之前的:
代码语言:javascript复制with (yield from lock):
...
异步迭代器和 async for
所谓异步迭代器,是一种可以在 iter 和 next 方法中调用异步代码的迭代器。要想实现它:
- 指定对象必须实现一个返回异步迭代器对象的
__aiter__
方法(如果是通过 CPython C API 定义则需要定义tp_as_async.am_aiter
slot 代替__aiter__
); - 异步迭代器对象必须实现一个返回可等待对象的
__anext__
方法(如果是通过 CPython C API 定义则需要定义tp_as_async.am_anext
slot 代替__anext__
); - 为了使迭代过程不会无限进行下去,
__anext__
必须在适当的时候抛出StopAsyncIteration
异常。
异步迭代器示例:
代码语言:javascript复制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):
...
新语法
代码语言:javascript复制async for TARGET in ITER:
BLOCK
else:
BLOCK2
语义上等同于:
代码语言:javascript复制iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
try:
TARGET = await type(iter).__anext__(iter)
except StopAsyncIteration:
runnint = False
else:
BLOCK
else:
BLOCK2
在 async for
后使用未实现 __aiter__
方法的常规迭可迭代对象会抛出 TypeError
异常,在 async def
外使用 async for
会抛出 SyntaxError
异常。
与常规 for
语句一样,async for
也有一个可选的 else
字句。
示例 1
通过异步迭代器可以在迭代过程中异步缓冲数据:
代码语言:javascript复制async for data in cursor:
...
其中,cursor
是一个异步迭代器,每迭代 N 次就会从数据库中预取 N 行数据。
下面的代码实现了异步迭代协议:
代码语言:javascript复制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)
等同于下面这段代码:
代码语言:javascript复制i = Cursor().__aiter__()
while True:
try:
row = await i.__anext__()
except StopAsyncIteration:
break:
else:
print(row)
示例 2
下面的示例是一个通用的工具类,它能够将常规迭代器转换为异步迭代器。虽然这不是一个会经常使用的操作,但是这个示例代码说明了常规迭代器和异步迭代器之间的关系:
代码语言:javascript复制class AsyncIteratorWrapper:
def __init__(self, obj):
self._it = iter(obj)
def __aiter__(self):
return self
async def __anext__(self):
try:
value = next(self._it)
expect StopIteration:
raise StopAsyncIteration
return value
async for letter in AsyncIteratorWrapper("abc"):
print(letter)
为什么需要 StopAsyncIteration
为什么需要 StopAsyncIteration
也就是为什么不继续使用 StopIteration
。协程的本质是生成器,所以在 PEP 479 之前,下面的两段代码没有本质上的不同:
def g1():
yield from fut
return "spam"
和
代码语言:javascript复制def g2():
yield from fut
raise StopIteration("spam")
由于 PEP 479 被接受并且在协程中默认启用,下面的示例代码将会使用 RuntimeError
包裹 StopIteration
:
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
异常; - 可以使用
yield from
加原生协程返回生成器式协程(在 asyncio 代码中必须使用@asyncio.coroutine
); inspect.isgenerator()
和inspect.isgeneratorfunction()
在接收原生协对象和原生协程方法时需要返回False
。
协程对象的内置方法
在底层实现上,协程继承自生成器共享实现代码。所以,协程类似生成器拥有 throw()
,send()
和 close()
方法,StopIteration
和 GeneratorExit
在协程中也起相同的作用(尽管 PEP 479 默认在协程中启用)。协程的 throw()
和 send()
方法被用来将值或异常传递给类 Future 对象。
更多细节请看 PEP 342,PEP 380 和 Python 文档相关章节。
用于调试的特性
注:asyncio.coroutine
在 Python 3.8 之后被标记为废弃,并在 Python 3.11 正式删除。
注:被标记为废弃的是 asyncio.coroutine
而不是 types.coroutine
注:这一小节的内容我看完之后有点犯迷糊,不知道他在表述什么事情。
新手容易犯的一个错误是忘记可以在协程中使用 yield from
:
@asyncio.coroutine
def useful():
# 如果没有 yield from 语句,这段代码将不会起作用
asyncio.sleep(1)
为了调试这类错误,asyncio 中有一种特殊的调试模式,其中 @coroutine
装饰器使用一个特殊的对象包装所有传递进来的函数,这个对象的析构函数会记录警告日志。每当被包装的生成器被 GC 进行垃圾回收时,就会产生一条详细的日志信息,其中包含该装饰器确切的定义位置、被回收位置的堆栈跟踪等信息。封装对象还提供了一个方便的 __repr__
函数,一种包含有关生成器的详细信息。
问题是如何启动这些调试功能。调试功能在生产环境下应该是不可用的,所以 @coroutine
装饰器根据操作系统环境变量 PYTHONSYNCIODEBUG
来判断是否起作用。这样就可以在运行 asyncio 程序时使用 asyncio 自带的函数。EventLoop.set_debug
(一种不用的调试工具)对 @coroutine
装饰器的行为没有影响。
为了使协程就成为与生成器不同的原生概念:
- 如果协程未被 await 直接调用会抛出
RuntimeWarning
异常; - 还建议在
sys
模块中添加两个新函数:set_coroutine_wrapper
和get_coroutine_wrapper
。它们的作用是在 asyncio 或其他框架中启用高级调试功能(例如显示创建协程的具体位置,以及更详细的垃圾回收堆栈跟踪)。
新内置函数
types.coroutine(gen)
:点击 types.coroutine()(https://wiki.blanc.site/#types.coroutine()) 了解更多;inspect.iscoroutine(obj)
:如果obj
是原生协程对象,返回True
;inspect.iscoroutinefunction(obj)
:如果obj
是原生协程函数,返回Ture
;inspect.isawaitable(obj)
:如果obj
是可等待对象,返回True
;inspect.getcoroutinestate(coro)
:返回原生协程对象的当前状态(inspect.getfgeneratorstate(gen)
的逆向函数);inspect.getfgeneratorstate(gen)
:返回本地协程独享的局部变量与其值的映射(inspect.getcoroutinestate(coro)
的逆向函数);sys.set_coroutine_wrapper(wrapper)
:允许拦截原生协程的创建,在原生协程创建时调用wrapper
。wrapper
可以是「一个接受一个参数(一个协程对象)的可调用对象」或是None
。如果是None
则会重置之前定义的wrapper
,如果调用多次,新的 wrpaaer 将取代之前的。该函数是线程绑定的;sys.get_coroutine_wrapper()
:返回通过sys.set_coroutine_wrapper
设置的wrapper
,如果没设置则返回None
。该函数是线程绑定的。
新的抽象基类
为了更好的与现有框架(如 Tornado,参考 引用 13)和编译器(如 Cython,参考 引用 16)集成,新增了两个抽象基类:
collections.abc.Awaitable
:为类 Future 对象创建的基类,实现了__await__
方法;collection.abc.Coroutine
:为协程对象创建的基类,实现了send(value)
,throw(type, exc, tb)
,close
和__await__()
方法。
注意,带有 CO_ITERABLE_COROUTINE
标志的生成器式协程没有实现 __await__
方法,因此不是 collections.abc.Coroutine
或 collections.abc.Awaitable
基类的实例:
@types.coroutine
def gencoro():
yield
assert not isinstance(gencoro(), collections.abc.Coroutine)
# 应该如何识别:
assert inspect.isawaitable(gencoro())
为了能更简单地测试指定对象是否支持异步迭代,又引入了另外两个基类:
collections.abc.AsyncIterable
:测试是否存在__aiter__
方法;collections.abs.AsyncIterator
:测试是否存在__aiter__
和__anext__
方法。
术语表
原生协程函数
Navite coroutine function,通过 async def
定义的协程函数,点击 原生协程声明语法 了解更多。
原生协程
Navite coroutine,从原生协程函数返回的内容,点击 await 表达式 了解更多。
生成器式协程函数
Generator-based coroutine function,基于生成器语法的协程,更常见的示例是使用 @asyncio.coroutine
定义的函数。
生成器式协程
Generator-based coroutine,通过生成器式协程函数返回的内容。
协程
Coroutine,原生协程或生成器式协程。
协程对象
Coroutine object,原生协程对象或生成器式协程对象。
类 Future 对象
Future-like object,拥有 __await__
方法的对象或拥有 tp_as_async->am_await
函数的 C Object,且该函数或方法返回一个迭代器。可以在协程中作为 await
表达式的参数。在协程中 await
类 Future 对象时,协程会被推迟直到类 Future 对象的 __await__
完成并且返回结果,点击 await 表达式 了解更多。
可等待对象
Awaitable,类 Future 对象或协程对象。点击 await 表达式 了解更多。
异步上下文管理器
Asynchronous context manager,拥有 __aenter__
和 __aexit__
方法的对象,可以搭配 async with
使用,点击 异步上下文管理器与 async with 了解更多。
异步可迭代对象
Asynchronous iterable,拥有 __aiter__
方法的对象,该方法返回一个异步迭代器对象。可以搭配 async for
一起使用,点击 异步迭代器和 async for 了解更多。
异步迭代器
Asynchronos iterator,拥有 __anext__
方法的对象,点击 异步迭代器和 async for 了解更多。
过渡计划
tokenizer.c
文件是 CPython 源码中的一个文件,主要负责实现 Python 解释器中的词法分析器。
为了解决 async
和 await
的向后兼容性问题,需要对 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
# 协程函数能够被执行并且打印 42
async def ham():
print(getattr(Spam, "async"))
向后兼容性
为了兼容新语法,需要确保在现有的内置模块中不存在与 async
和 await
关键字冲突的命名,且新的原生协程需要兼容之前存在的生成器式协程。
asyncio
注:在本 PEP 实施之前,asyncio 库中已经存在了一个名为 async
的函数。
asyncio
模块进行了调整和测试,使现有协程方案与新语法保持兼容,保证 100% 向后兼容,即现有代码能够在新版本中正常运行。
进行调整的主要有:
- 使
@asyncio.coroutine
装饰器使用新的types.coroutine()
函数; - 向
asyncio.Future
类添加__await__ = __iter__
; - 将
ensure_future()
作为async()
函数的别名,废弃asyncio
中的async()
函数。
asyncio 迁移策略
yield from
原生协程对象不能返回普通的生成器(点击 与生成器的不同之处 了解更多),因此建议在开始使用新语法之前,确保所有生成器式协程都使用 @asyncio.coroutine
进行装饰。
CPython 代码库中的 async/await
在 CPython 中没有使用 await
。
async
关键字主要是被 asyncio 模块占用。为了解决这个问题,需要将 asyncio 模块中的 async()
函数重命名为 ensure_future()
(点击 asyncio 了解更多)。
async
关键字的另一个占用场景是 Lib/xml/dom/xmlbuilder.py
中为 DocumentLS
类定义的 async = False
属性。没有针对这一属性的文档或测试文件,CPython 中的其他地方也没有使用这个属性。现在它被一个 getter 取代,调用 getter 会引发一个 DeprecationWarning
异常并通过异常信息建议使用 async_
属性代替此属性。 除此以外,CPython 代码库中没有其他的 async 属性被记录或使用。
语法更新
语法的变化相当小:
代码语言:javascript复制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 tarilter*
# 这段代码定义了 Python 中异步语法的各个组成部分,
# 包括异步函数定义、异步语句以及与异步操作相关的表达式。
# 这些语法元素共同构成了 Python 异步编程的基础。
# 来自 Google Gemini 1.5 Pro
废弃计划
注:根据原文,本来计划在 Python 3.5 或 3.6 中废弃 async
和 await
**,并在 3.7 中过渡到一个更合适的关键字,但从当下来看这个计划应该是没有实施。**
决策过程
PEP 3152
Gregory Ewing 提出的 PEP 3152 提供了另一种机制来实现协程(或者称为 cofunctions),其中一些关键的要素:
- 用于声明 cofunction 的新关键字
codef
。Cofunction 总是一个生成器,即使没有cocall
表达式在其内部。类比async def
; - 用于调用 cofunction 的新关键字
cocall
。只能够被用在 cofunction 内部。类比await
; - Cofunction 只能通过
cocall
关键字调用; cocall
语法需要在其后方使用圆括号;cocall f(*args, **kwargs)
在语义上等同于yield from f.__cocall__(*args, **kwds)
。
相关语法定义:
代码语言:javascript复制atom: cocall | <existing alternatives for atom>
cocall: 'cocall' atom cotrailer* '(' [arglist] ')'
cotrailer: '[' subscriptlist ']' | '.' NAME
与本提案的不同之处:
- 没有与
__cocall__
一致的方法。__cocall__
方法会被cocall
表达式会调用并将其结果传递给yield from
,虽然__await__
方法与__cocall__
类似,但__await__
只用于定义类 Future 对象。 - 在语法中,
await
的定义几乎与yield from
相同(后来强制规定await
只能出现在async def
中)。但await
可以很简洁地使用await future
的方式调用,而cocall
总是需要圆括号辅助; - 要使 asyncio 与 PEP 3152 兼容,需要重构
@asyncio.coroutine
装饰器,来将所有函数封装在一个带有__cocal__
方法的对象中,或在生成器上实现__cocall__
。要在生成器式协程中调用 cofunctions,需要使用内置的costart(cofunc, *args, **kwargs)
; - 因为 cofunction 必须使用
cocall
关键字调用 ,因此自动避免在生成器式协程中忘记使用yield from
的常见错误。本提案是使用其他方法来解决这一问题的,点击 用于调试的特性 了解更多。 - 使用
cocall
调用 cofunction 的一个缺点是,如果决定实现协程生成器(使用yield
或async yield
表达式的协程),就不需要cocall
关键字来调用。因此最终会使得协程拥有__cocall__
而没有__call__
,协程生成器拥有__call__
而没有__cocall__
。 - 在 PEP 3152 中, 没有类似
async for
和async with
的设计。 - 括号语法会带来很多问题:
下面这段代码:
代码语言:javascript复制await fut
await function_returning_future()
await asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))
需要这样表达:
代码语言:javascript复制cocall fut()
cocall (function_returning_future())
cocall asyncio.gather(costart(coro1, arg1, arg2), costar(coro2, arg1, arg2))
协程生成器
通过 async for
关键字可以实现一种协程生成器的概念,即一个带有 yield
或 yield from
的协程。为了避免与一般的生成器混淆,可能需要在 yield
关键字前加上 async
关键字,而 async yield from
会抛出 StopAsyncIteration
异常。
虽然协程生成器的概念可能实现,但是不应该在本提案中讨论。这是一个高阶的概念,会使当前生成器的实现发生巨大的变动,应该权衡利弊,仔细考虑。这个问题应该由一个单独的 PEP 进行讨论。
为什么选择 async 和 await 关键字
在众多编程语言中,async/await 已经不是一个新鲜的概念了:
- C# 很久以前就是使用它们,请看 引用 5;
- ECMAScript 7 中也提议键入 async/await,在 Traceur 项目也是,请看 引用 2 和 引用 9;
- Facebook’s Hack/HHVM,请看 引用 6;
- Googles 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 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
可以理解为从 iterator 中遍历异步的 item。
将 async
放在 def
、with
和 for
前面,还能使语言语法更加简单。同时,async def
也能让使用者更方便地区分协程与普通函数。
为什么不导入 __future__
from __future__ import feature
形式的导入被称为 future 语句。 它们会被 Python 编译器当作特例,通过包含 future 语句来允许新的 Python 特性在该特性成为语言标准之前发布的模块中使用。
过渡计划 章节介绍了词法分析器做了哪些修改使其仅在 async def
块才中将 async
和await
作为关键字处理。因此,async def
发挥了模块级编译器声明(类似 from __future__ import async_await
)的作用。
为什么异步魔术方法都用 a 开头
一个备选方案是使用 async
前缀,但是为了让新的魔术方法和原有魔术方法保持尽可能高的相似性,最终选择了方法名更短的方案。
为什么不复用现有魔术方法
存在一个异步迭代器和异步上下文管理器的备选方案,提议在声明中添加 async
关键字来复用现有的魔术方法:
class CM:
# 代替 __aenter__
async def __enter__(self):
...
这个方案有以下缺点:
- 不能创建一个既可以在
with
中使用,又可以在async with
中使用的对象; - 会破坏兼容性,因为在版本低于 3.4 的 Python 代码中没有规定禁止从
__enter__
或__exit__
中返回类 Future 对象; - 让原生协程简洁无歧义是本提案主要目的之一,因此将异步协议用的魔术方法做了区分处理。
为什么复用 for 和 with 语句
无论是现有的生成器式协程还是本提案提出的原生协程都更希望使用者能够明显地看到代码可能阻塞的位置。让现有的 for
和 with
语句识别异步迭代器和异步上下文管理器会不可避免地引入隐式阻塞点,从而导致代码变得更难理解。
异步推导式
可以提供异步推导式,但是这个语法不在本提案的讨论范围内。
注:PEP 530 定义了异步推导式,可以在 3.6 之后的版本使用。
异步 lambda 函数
可以提供异步 lambda 函数,但这个语法不在本提案的讨论范围内。
注:目前 Python 还没有解锁异步 lambda。
性能影响
整体影响
本提案不会对 Pyhton 本身的性能产生明显影响。下面是 Python 官方基准测试 的输出结果:
代码语言:javascript复制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 的影响
下面的微型基准测试用于确定异步函数和生成器之间的性能差异:
代码语言:javascript复制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, repate):
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))
结果是没有观察到明显的性能差异:
代码语言:javascript复制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
注 depth = 19 以为着 1048575 次调用。
实施
可以通过 引用 15 追踪具体实施过程,它在 2015-5-11 提交。
引用
- https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine
- http://wiki.ecmascript.org/doku.php?id=strawman:async_functions
- https://github.com/1st1/cpython/tree/await
- https://hg.python.org/benchmarks
- https://msdn.microsoft.com/en-us/library/hh191443.aspx
- http://docs.hhvm.com/manual/en/hack.async.php
- https://www.dartlang.org/articles/await-async/
- http://docs.scala-lang.org/sips/pending/async.html
- https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental
- http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf
- https://docs.python.org/3/reference/expressions.html#generator-iterator-methods
- https://docs.python.org/3/reference/expressions.html#primaries
- https://mail.python.org/pipermail/python-dev/2015-May/139851.html
- https://mail.python.org/pipermail/python-dev/2015-May/139844.html
- http://bugs.python.org/issue24017
- https://github.com/python/asyncio/issues/233
- https://hg.python.org/cpython/rev/7a0a1a4ac639
- http://bugs.python.org/issue24400
- http://bugs.python.org/issue27243
- https://docs.python.org/3/reference/datamodel.html#async-iterators
参考
- PEP 492 – Coroutines with async and await syntax