原文地址: PEP-0492
PEP | 492 |
---|---|
标题 | 协程与async/await语法 |
作者 | Yury Selivanov <yury at magic.io> |
翻译 | ipfans <ipfanscn at gmail.com> |
状态 | 最终稿 |
Python版本 | 3.5 |
翻译最后更新 | 2015-11-03 |
目录
设计思路(暂时不考虑翻译)
性能
参考
致谢
版权信息
摘要
不断增长的网络连通性需求带动了对响应性、伸缩性代码的需求。这个PEP的目标在于回答如何更简单的、Pythinic的实现显式的异步/并发的Python代码。
我们把协程概念独立出来,并为其使用新的语法。最终目标是建立一个通用、易学的Python异步编程模型,并尽量与同步编程的风格保持一致。
这个PEP假设异步任务被一个事件循环器(类似于标准库里的 asyncio.events.AbstractEventLoop)管理和调度。不过,我们并不会依赖某个事件循环器的具体实现方法,从本质上说只与此相关:使用yield作为给调度器的信号,表示协程将会挂起直到一个异步事件(如IO)完成。
我们相信这些改变将会使Python在这个异步编程快速增长的领域能够保持一定的竞争性,就像许多其它编程语言已经、将要进行的改变那样。
API设计和实现的备注
根据Python 3.5 Beta期间的反馈,我们进行了重新设计:明确的把协程从生成器里独立出来—原生协程现在拥有了自己完整的独立类型,而不再是一种新的生成器类型。
这个改变主要是为了解决在Tornado Web服务中里集成协程时出现的一些问题。
基本原理和目标
现在版本的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)和Cofunctions
提议(PEP-3152,现在此提议已被废弃)。
从本文档中,我们使用原生协程
代指新语法生命的函数,基于生成器的协程
用于表示那些基于生成器语法实现的协程。协程
则表示两个地方都可以使用的内容。
新协程声明语法
下面的新语法用于声明原生协程:
1 | async def read_data(db): |
协程的主要属性包括:
async def
函数始终为协程,即使它不包含await
表达式。- 如果在
async
函数中使用yield
或者yield from
表达式会产生SyntaxError
错误。 - 在内部,引入了两个新的代码对象标记:
CO_COROUTINE
用于标记原生协程(和新语法一起定义)CO_ITERABLE_COROUTINE
用于标记基于生成器的协程,兼容原生协程。(通过types.coroutine()
函数设置)
- 常规生成器在调用时会返回一个
genertor
对象,同理,协程在调用时会返回一个coroutine
对象。 - 协程不再抛出
StopIteration
异常,而是替代为RuntimeError
。常规生成器实现类似的行为需要进行引入__future__
(PEP-3156) - 当协程进行垃圾回收时,一个从未被
await
的协程会抛出RuntimeWarning
异常。(参考调试特性) - 更多内容请参考协程对象一节。
types.coroutine()
在types
模块中新添加了一个函数coroutine(fn)
用于asyncio
中基于生成器的协程与本PEP中引入的原生携协程互通。
1 |
|
这个函数将生成器函数对象设置CO_ITERABLE_COROUTINE
标记,将返回对象变为coroutine
对象。
如果fn
不是一个生成器函数,那么它会对其进行封装。如果它返回一个生成器,那么它会封装一个awaitable
代理对象(参考下面awaitable
对象的定义)。
注意:CO_COROUTINE
标记不能通过types.coroutine()
进行设置,这就可以将新语法定义的原生协程与基于生成器的协程进行区分。
types模块添加了一个新函数coroutine(fn),使用它,“生成器实现的协程”和“原生协程”之间可以进行互操作。
Await表达式
下面新的await
表达式用于获取协程执行结果:
1 | async def read_data(db): |
await
与yield from
相似,挂起read_data
协程的执行直到db.fetch
这个awaitable
对象完成并返回结果数据。
它复用了yield from
的实现,并且添加了额外的验证参数。await
只接受以下之一的awaitable
对象:
- 一个原生协程函数返回的原生协程对象。
- 一个使用
types.coroutine()
修饰器的函数返回的基于生成器的协程对象。 - 一个包含返回迭代器的
__await__
方法的对象。
任意一个yield from
链都会以一个yield
结束,这是Future
实现的基本机制。因此,协程在内部中是一种特殊的生成器。每个await
最终会被await
调用链条上的某个yield
语句挂起(参考PEP-3156中的进一步解释)。
为了启用协程的这一特点,一个新的魔术方法__await__
被添加进来。在asyncio
中,对于对象在await语句启用Future
对象只需要添加__await__ = __iter__
这行到asyncio.Future
类中。
在本PEP中,带有__await__
方法的对象也叫做Future-like
对象。
同样的,请注意到__aiter__
方法(下面会定义)不能用于这种目的。它是不同的协议,有点类似于用__iter__
替代普通调用方法的__call___
。
如果__await__
返回非迭代器类型数据,会产生一个TypeError
. - CPython C API中使用
tp_as_async.am_await
定义的函数,并且返回一个迭代器(类似__await__
方法)。
新的操作符优先级列表
关键词await
与yield
和yield form
操作符的区别是await
表达式大部分情况下不需要括号包裹。
同样的,yield from
允许允许任意表达式做其参数,包含表达式如yield a()+b()
,这样通常处理作为yield from (a()+b())
,这个通常会造成Bug。通常情况下任意算数操作的结果都不会是awaitable
对象。为了避免这种情况,我们将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__
。这两个方法都返回awaitable
对象。
异步上下文管理器例子如下:
1 | class AsyncContextManager: |
新语法
一个新的异步上下文管理语法被接受:
1 | async with EXPR as VAR: |
语义上等同于:
1 | mgr = (EXPR) |
和普通的with
语句一样,可以在单个async with
语句里指定多个上下文管理器。
在使用async with
时,如果上下文管理器没有__aenter__
和__aexit__
方法,则会引发错误。在async def
函数之外使用async with
则会引发SyntaxError
异常。
例子
通过异步上下文管理器更容易实现协程对数据库事务的正确管理:
1 | async def commit(session, data): |
代码看起来也更加简单:
1 | async with lock: |
而不是
1 | with (yield from lock): |
异步迭代器与async for
一个异步迭代器能够在它的迭代实现里调用异步代码,也可以在它的__next__
方法里调用异步代码。为了支持异步迭代,需要:
- 一个对象必须实现
__aiter__
方法(或者,使用CPython C API的tp_as_async.am_aiter定义),返回一个异步迭代器对象中的`awaitable``结果。 - 一个异步迭代器必须实现
__anext__
方法(或者,使用CPython C API的tp_as_async.am_anext定义)返回一个awaitable
。 - 停止迭代器的
__anext__
必须抛出一个StopAsyncIteration
异常。
一个异步迭代的例子:
1 | class AsyncIterable: |
新语法
一种新的异步迭代方案被采纳:
1 | async for TARGET in ITER: |
语义上等同于:
1 | iter = (ITER) |
如果对一个普通的不含有__aiter__
方法的迭代器使用async for
,会引发TypeError
异常。如果在async def
函数外使用async for
会已发SyntaxError
异常。
和普通的for
语法一样,async for
有可选的else
分支。
例子1
通过异步迭代器,就可以实现通过迭代实现异步缓冲数据:
1 | async for data in cursor: |
当cursor
是一个异步迭代器时,就可以在N次迭代后从数据库中预取N行数据。
下面的代码演示了新的异步迭代协议:
1 | class Cursor: |
那么这个Cursor
类可以按照下面的方式使用:
1 | async for row in Cursor(): |
这个等同于下面的代码:
1 | i = await Cursor().__aiter__() |
例子2
下面的工具类用于将普通的迭代转换为异步。这个并没有什么实际的作用,这个代码只是用于演示普通迭代与异步迭代之间的关系。
1 | class AsyncIteratorWrapper: |
为什么使用StopAsyncIteration
协程在内部实现中依旧是依赖于迭代器的。因此,在PEP-479生效之前,下面两者并没有区别:
1 | def g1(): |
但是在PEP 479接受并且默认对协程开启时,下面的例子中的StopIteration
会被封装成RuntimeError
。
1 | async def a1(): |
所以,想通知外部代码迭代已经结束,抛出一个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
)可以yield from
一个原生协程。 - 对原生协程对象和原生协程函数调用
inspect.isgenerator()
和inspect.isgeneratorfunction()
会返回False。
协程对象方法
协程内部基于生成器,因此他们同享实现过程。类似于生成器对象,协程包含throw()
,send()
和close()
方法。StopIteration
和GeneratorExit
在协程中扮演者同样的角色(尽管PEP 479默认对协程开启了)。参考PEP-342, PEP-380和Python文档了解更多细节。
协程的throw()
和send()
方法可以用于将返回值和抛出异常推送到类似于Future
的对象中。
调试特性
一个初学者普遍会犯的错误是忘记在协程中使用yield from
。
1 |
|
为了调试这类错误,asycio提供了一种特殊的调试模式:装饰器@coroutine
封装所有的函数成一个特殊对象,这个对象的析构函数中记录警告。当封装的生成器垃圾回收时,会产生详细的记录信息,包括具体定义修饰函数、回收时的栈信息等等。封装对象同样提供一个__repr__
函数用于输出关于生成器的详细信息。
唯一的问题是如何启用这些调试功能。这些调试工具在生产模式中什么都不做,@coroutine
修饰符在系统变量PYTHONASYNCIODEBUG
设置后才会提供调试功能。这种方式可以让asyncio程序使用asyncio自己的函数分析。EventLoop.set_debug
是另外一个调试工具,他不会影响@coroutine
修饰符行为。
根据本提议,协程是原生的与生成器不同的概念。当抛出RuntimeWarning
异常的协程是从来没有被awaited
过的。因此添加了两条新的函数到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是awaitable
时返回为True。inspect.getcoroutinestate(coro)
返回原生协程对象的当前状态(是inspect.getfgeneratorstate(gen)
的镜像)。inspect.getcoroutinelocals(coro)
返回原生协程对象的局部变量的映射(是inspect.getgeneratorlocals(gen)
的镜像)。sys.set_coroutine_wrapper(wrapper)
允许拦截原生协程对象的创建。wrapper
必须是一个接受一个参数callable
(一个协程对象),或者是None
。None
会重置wrapper
。当调用第二次时,新的wrapper
会替代之前的封装。这个函数是线程专有的。参考调度调试了解更多细节。sys.get_coroutine_wrapper()
返回当前的封装对象。如果封装未设置会返回None。这个函数是线程专有的。参考调度调试了解更多细节。
新的抽象基类
为了允许更好的与现有的框架(比如Tornado)和编译器(比如Cython)整合,我们添加了两个新的抽象基类(ABC)collections.abc.Awaitable
是Future-like
类的抽象基类,它实现了__await__
方法。collections.abc.Coroutine
是协程对象的抽象基类,它实现了 send(value)
,throw(type, exc, tb)
,close()
和__await__()
方法。
值得注意的是,带有CO_ITERABLE_COROUTINE
标记的基于生成器的协程并没有实现__await__
方法,因此他不是collections.abc.Coroutine
和collections.abc.Awaitable
抽象类的实例:
1 |
|
为了方便对异步迭代的调试,添加了另外两个抽象基类:
collections.abc.AsyncIterable
– 用于测试__aiter__
方法collections.abc.AsyncIterator
– 用于测试__aiter__
和__anext__
方法。
专用术语表
函数与方法列表
移植计划
向后兼容性
asyncio
asyncio移植策略
CPython代码中的async/await
语法更新
失效计划
性能
总体影响
这个提议并不会造成性能影响。这是Python官方性能测试结果:
1 | python perf.py -r -b default ../cpython/python.exe ../cpython-aw/python.exe |
编译器修改
修改后的编译器处理Python文件没有明显的性能下降:处理12MB大小的文件(Lib/test/test_binop.py
重复1000次)消耗时间相同。
async/await
下面的小测试用于检测『async』函数和生成器的性能差异:
1 | import sys |
结果显示并没有明显的性能差异:
1 | binary(19) * 30: total 53.321s |
注意:19层意味着1,048,575调用。
实现参考
实现参考可以在这里找到。
上层修改和新协议列表
- 新的协程定义语法:
async def
和新的await
关键字。 Future-like
对象提供新的__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
。 - 新的抽象基类
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提出的所有概念都已经实现,并且可以被测试。
1 | import asyncio |