原文标题:PEP 0492 -- Coroutines with async and await syntax
原文连接:https://www.python.org/dev/peps/pep-0492/
生效于:Python 3.5
翻译参照版本:05-May-2015
翻译最后修改:2015年8月22日
翻译出处:http://www.cnblogs.com/animalize/p/4738941.htmlhtml
用几句话说明这个PEP:python
- 把协程的概念从生成器独立出来,并为之添加了新语句(async/await)。
- 可是在CPython的内部实现,协程仍然是一个生成器。
- 增长了异步迭代器(async for),异步迭代器的__aiter__、__anext__函数是协程,能够将程序挂起。
- 增长了异步上下文管理器(async with),异步上下文管理器的__aenter__、__aexit__函数是协程,能够将程序挂起。
不断增多的Internet链接程序刺激了对响应性、伸缩性代码的需求。这个PEP的目标在于:制订显式的异步/并发语法,比传统的Python方法更易用、更丰富。程序员
咱们准备把协程(协同程序)的概念独立出来,并为其使用新的语法。最终目标是创建一个通用、易学的异步编程的构思模型,并尽可能与同步编程的风格类似。数据库
这个PEP假设异步任务被一个事件循环器(相似于标准库里的 asyncio.events.AbstractEventLoop)管理和调度。然而咱们并不会依赖某个事件循环器的具体实现方法,从本质上说只与此相关:采用yield做为给调度器的信号,表示协程将会挂起、等待一个异步事件(如IO)的完成。编程
在这个异步编程不断增加的时期,咱们相信这些改变将会使Python保持必定的竞争性,就像许多其它编程语言已经、将要进行的改变那样。api
根据Python 3.5 Beta期间的反馈,进行了从新设计,明确地把协程从生成器里独立出来了。协程如今是原生的,有明确的独立类型,而不是做为生成器的一种特殊形式。缓存
这个改变,主要是为了解决在Tornado里使用协程出现的一些问题。
【译注:在Tornado 4.3已经可使用新的async/await语句,详见此连接】session
在之前,咱们能够用生成器实现协程(PEP 342),后来又对其进行了改进,引入了yield from语法(PEP 380)。但仍有一些缺点:并发
这个PEP把协程从生成器独立出来,成为Python的一个原生事物。这会消除协程和生成器之间的混淆,方便编写不依赖特定库的协程代码。也为linter和IDE进行代码静态分析提供了机会。
【译注:在CPython内部,原生协程仍然是基于生成器实现的。】app
使用原生协程和相应的新语法,咱们能够在异步编程时使用上下文管理器(context manager)和迭代器。以下文所示,新的async with语句能够在进入、离开运行上下文(runtime context)时进行异步调用,而async for语句能够在迭代时进行异步调用。
请理解Python现有的协程(见PEP 342和PEP 380),此次改变的动机来自于asyncio框架(PEP 3156)和Confunctions提案(PEP 3152,此PEP已经被废弃)。
由此,在本文中,咱们使用“原生协程”指用新语法声明的协程。“生成器实现的协程”指用传统方法实现的协程。“协程”则用在两个均可以使用的地方。
使用如下语法声明原生协程:
async def read_data(db): pass
协程语法的关键点:
types模块添加了一个新函数coroutine(fn),使用它,“生成器实现的协程”和“原生协程”之间能够进行互操做。
【译注:这是个装饰器,能把现有代码的“用生成器实现的协程”转化为与“原生协程”兼容的形式】
@types.coroutine def process_data(db): data = yield from read_data(db) ...
coroutine(fn)函数给生成器的代码对象(code object)设置CO_ITERABLE_COROUTINE标识,使它返回一个协程对象。
若是fn不是一个生成器函数,它什么也不作。若是fn是一个生成器函数,则会被一个awaitable代理对象(proxy object)包装(wrapped),详见下文的“定义awaitable对象”。
注意, types.coroutine()不会设置CO_COROUTINE标识,只有用新语法定义的原生协程才会有这个标识。
【译注: @types.coroutine装饰器仅给生成器函数设置一个CO_ITERABLE_COROUTINE标识,除此以外什么也不作。可是若是生成器函数没有这个标识,await语句不会接受它的对象做为参数。】
新的await表达式用于得到协程执行的结果:
async def read_data(db): data = await db.fetch('SELECT ...') ...
await和yield from相似,它挂起read_data的执行,直到db.fetch执行完毕并返回结果。
以CPython内部,await使用了yield from的实现,但加入了一个额外步骤——验证它的参数类型。await只接受awaitable对象,awaitable对象是如下的其中一个:
一个有__await__方法的对象(__await__方法返回的一个迭代器)
每一个yield from调用链条都会追溯到一个最终的yield语句,这是Future实现的基本机制。在Python内部,因为协程是生成器的一种特殊形式,因此每一个await最终会被await调用链条上的某个yield语句挂起。(详情请参考PEP 3156)
【译注:Future对象用来表示在将来完成的某项任务。】
为了让协程也有这样的行为,添加了一个新的魔术方法__await__。【译注:一系列递归调用必终结于某个return具体结果的语句;一个yield from调用链条必终结于某个yield语句;相似的,一个await调用链条必终结于某个有__await__方法的对象。】例如,在asyncio模块,要想在await语句里使用Future对象,惟一的修改是给asyncio.Future加一行:__await__ = __iter__
在本文中,有__await__方法的对象被称为Future-like对象。
【译注:协程会被await语句挂起,直到await语句右边的Future-like对象的__await__执行完毕、返回结果。】
另外,请注意__aiter__方法(见下文)不能被用于此目的。那是另外一套东西,这样作的话,相似于callable对象使用__iter__代替__call__。【译注:意思是__await__和__aiter__的关系有点像callable对象里__call__和__iter__的关系】
若是__await__返回的不是一个迭代器,则引起TypeError异常。在CPython C API,有tp_as_async.am_await函数的对象,该函数返回一个迭代器(相似__await__方法)
若是在async def函数以外使用await语句,会引起SyntaxError异常。这和在def函数以外使用yield语句同样。
若是await右边不是一个awaitable对象,会引起TypeError异常。
【译注:整体略去不译。】
await语句和yield、yield from的一个区别是: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()) |
异步上下文管理器(asynchronous context manager),能够在它的enter和exit方法里挂起、调用异步代码。
为此,咱们设计了一套方案,添加了两个新的魔术方法:__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__(mgr) exc = True VAR = await aenter try: BLOCK except: if not await aexit(mgr, *sys.exc_info()): raise else: await aexit(mgr, None, None, None)
和普通的with语句同样,能够在单个async with语句里指定多个上下文管理器。
在使用async with时,若是上下文管理器没有__aenter__和__aexit__方法,则会引起错误。在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): ...
异步迭代器能够在它的iter实现里挂起、调用异步代码,也能够在它的__next__方法里挂起、调用异步代码。要支持异步迭代,须要:
异步迭代的一个示例:
class AsyncIterable: async 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 = await type(iter).__aiter__(iter) running = True while running: try: TARGET = await type(iter).__anext__(iter) except StopAsyncIteration: running = False else: BLOCK else: BLOCK2
若是async for的迭代器不支持__aiter__方法,则引起TypeError异常。若是在async def函数外使用async for,则引起SyntaxError异常。
和普通的for语句同样,async for有一个可选的else分句。
有了异步迭代,咱们能够在迭代时异步缓冲(buffer)数据:
async for data in cursor: ...
Cursor是一个异步迭代器,能够从数据库预读4行数据并缓存。见如下代码:
# 【译注:此代码已被修改,望更易理解】 class Cursor: def __init__(self): self.buffer = collections.deque() async def _prefetch(self): row1, row2, row3, row4 = await fetch_from_db() self.buffer.append(row1) self.buffer.append(row2) self.buffer.append(row3) self.buffer.append(row4) async 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 = await Cursor().__aiter__() while True: try: row = await i.__anext__() except StopAsyncIteration: break else: print(row)
这是一个便利类,用于把普通的迭代对象转变为一个异步迭代对象。虽然这个类没什么实际用处,但它演示了普通迭代器和异步迭代器的关系:
class AsyncIteratorWrapper: def __init__(self, obj): self._it = iter(obj) async 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)
在CPython内部,协程的实现仍然是基于生成器的。因此,在PEP 479生效以前【译注:将在Python 3.7正式生效,在3.五、3.6须要from __future__ import generator_stop】,如下两个代码是彻底同样的,最终都是给外部代码抛出一个StopIteration('spam')异常:
def g1(): yield from fut return 'spam'
和
def g2(): yield from fut raise StopIteration('spam')
因为PEP 479已被正式采纳,并做用于协程,如下代码的StopIteration会被包装(wrapp)成一个RuntimeError。
async def a1(): await fut raise StopIteration('spam')
因此,要想通知外部代码迭代已经结束,抛出一个StopIteration异常的方法不行了。所以,添加了一个新的内置异常StopAsyncIteration,用于表示迭代结束。
此外,根据PEP 479,协程抛出的全部StopIteration异常都会被包装成RuntimeError异常。
【译注:若是函数生成器内部的代码出现StopIteration异常、且未被捕获,则外部代码会误认为生成器已经迭代结束。为了消除这样的误会,PEP 479的规定,Python会把生成器内部抛出的StopIteration包装成RuntimeError。
在之后,若是想主动结束一个函数生成器的迭代,用return语句便可(这时函数生成器仍然会给外部代码抛出一个StopIteration异常),而不是之前的使用raise StopIteration语句(这样的话,StopIteration会被包装成一个RuntimeError)。】
这一小节只对原生协程有效(用async def语法定义的、有CO_COROUTINE标识的)。对于asyncio模块里现有的“基于生成器的协程”,仍然保持不变。
为了在概念上把协程和生成器区分开来,作了如下规定:
【译注: @asyncio.coroutine装饰器,在Python 3.4,用于把一个函数装饰为一个协程。有些函数并非生成器函数(不含yield或yield from语句),也能够用 @asyncio.coroutine装饰为一个协程。
在Python 3.5中, @asyncio.coroutine也会有 @types.coroutine的效果——使函数的对象能够被await语句接受。】
在CPython内部,协程是基于生成器实现的,所以它们有共同的代码。像生成器对象那样,协程也有throw(),send()和close()方法。
对于协程,StopIteration和GeneratorExit起着一样的做用(虽然PEP 479已经应用于协程)。详见PEP 34二、PEP 380,以及Python文档。
对于协程,send(),throw()方法用于往Future-like对象发送内容、抛出异常。
新手在使用协程时可能忘记使用yield from语句,好比:
@asyncio.coroutine def useful(): asyncio.sleep(1) # 前面忘写yield from,因此程序在这里不会挂起1秒
在asyncio里,对于此类错误,有一个特定的调试方法。装饰器 @coroutine用一个特定的对象包装(wrap)全部函数,这个对象有一个析构函数(destructor)用于记录警告信息。不管什么时候,一旦被装饰过的生成器被垃圾回收,会生成一个详细的记录信息(具体哪一个函数、回收时的stack trace等等)。包装对象提供一个__repr__方法用来输出关于生成器的详细信息。
惟一的问题是如何启用这些调试工具,因为这些调试工具在生产模式里什么也不作,好比 @coroutine必须是在系统变量PYTHONASYNCIODEBUG出现时才具备调试功能。这时能够给asyncio程序进行以下设置:EventLoop.set_debug(true),这时使用另外一套调试工具,对 @coroutine的行为没有影响。
根据本文,协程是原生的,已经在概念上和生成器进行了区分。一个从未await的协程会抛出一个RuntimeWarning,除此以外,给sys模块增长了两个新函数set_coroutine_wrapper和get_coroutine_wrapper,它们会为asyncio和其它框架启用高级调试工具,好比显示协程在何处被建立、协程在何处被垃圾回收的详细stack trace。
为了能更好的与现有框架(如Tornado)和其它编译器(如Cython)相整合,增长了两个新的抽象基类(Abstract Base Classes):
注意,“基于生成器的协程”(有CO_ITERABLE_COROUTINE标识)并不实现__await__方法,所以它们不是collections.abc.Coroutine和collections.abc.Awaitable的实例:
@types.coroutine def gencoro(): yield assert not isinstance(gencoro(), collections.abc.Coroutine) # however: assert inspect.isawaitable(gencoro())
为了更容易地对异步迭代进行调试,又增长了两个抽象基类:
原生协程函数 Native coroutine function
由async def定义的协程函数,可使用await和return value语句。见“新的协程声明语法”一节。
原生协程 Native coroutine
原生协程函数返回的对象。见“await表达式”一节。
基于生成器的协程函数 Generator-based coroutine function
基于生成器语法的协程,最多见的是用 @asyncio.coroutine装饰过的函数。
基于生成器的协程 Generator-based coroutine
基于生成器的协程函数返回的对象。
协程 Coroutine
“原生协程”和“基于生成器的协程”都是协程。
协程对象 Coroutine object
“原生协程对象”和“基于生成器的协程对象”都是协程对象。
Future-like对象 Future-like object
一个有__await__方法的对象,或一个有tp_as_async->am_await函数的C语言对象,它们返回一个迭代器。Future-like对象能够在协程里被一条await语句消费(consume)。协程会被await语句挂起,直到await语句右边的Future-like对象的__await__执行完毕、返回结果。见“await表达式”一节。
Awaitable
一个Future-like对象或一个协程对象。见“await表达式”一节。
异步上下文管理器 Asynchronous context manager
有__aenter__和__aexit__方法的对象,能够被async with语句使用。见“异步上下文管理器和‘async with’”一节。
可异步迭代对象 Asynchronous iterable
有__aiter__方法的对象, 该方法返回一个异步迭代器对象。能够被async for语句使用。见“异步迭代器和‘async for’”一节。
异步迭代器 Asynchronous iterator
有__anext__方法的对象。见“异步迭代器和‘async for’”一节。
【译注:感受余下大部份内容没必要翻译,若有须要请参看原文。这里只挑选部份内容翻译。】
本PEP保持100%向后兼容。
asyncio模块已经可使用新语法,并通过测试,100%与async/await兼容。现有的使用asyncio的代码在使用新语法时能够保持不变。
为此,对asyncio模块主要作了以下修改:
因为未经装饰的生成器不能yield from原生协程对象(详见“和生成器的不一样之处”一节),所以在使用新语法前,请确保全部“基于生成器的协程”都被 @asyncio.coroutine装饰器装饰。
async和await在CPython 3.五、3.6里暂时不是正式的关键字,在CPython 3.7它们将变成正式的关键字。若是不这样,恐怕对现有代码的迁移形成困难。 【译注:在某些现有代码里,可能使用了async和await做为变量名/函数名。然而Python不容许把关键字看成变量名/函数名,因此3.五、3.6给程序员留了一些迁移时间。】