概要

Python 3.5 引入 async 和 await 使得协程成为 Python 原生特征(native feature),引入这两个新语法的原因主要可以概括为下面几点:

Python 原来的协程是通过生成器实现的,所以它们两者拥有相同的语法,详见 PEP 342 – Coroutines via Enhanced Generators 和 PEP 380 – Syntax for Delegating to a Subgenerator。虽然生成器和协程是两个不同的概念,但是由于 Python 通过生成器实现协程,所以很容易认为生成器和协程是相同的。通过引入 async ,我们可以更好的区分生成器和协程,消除了二义性。

异步调用受限于 yield 表达式能出现的地方,即我们只能实现异步函数。通过引入 async ,我们可以定义异步 for 循环和异步上下文管理器。

一个函数是否是协程取决于函数体中是否有 yield 或者 yield from,这样会导致重构代码过程中引入不明显的bug。async 能减少重构代码导致的 bug

async

async 可以被用来定义一个原生协程,其语法如下:

>>>async defread_data(db):

...pass

...

>>>r = read_data(None)

>>>type(r)

coroutine

鉴于 Python 的生成器也可做协程来使用,我们需要先做一下区分:

原生协程函数 使用 async def 声明的协程函数,函数内部使用 await 表达式和 return 语句

原生协程 由原生协程函数返回的对象

基于生成器的协程函数 基于生成器语法的协程函数。

基于生成器的协程 由基于生成器的协程函数返回的对象

协程 既可以是原生协程又可以是基于生成器的协程

协程对象 既可以是原生协程对象又可以是基于生成器的协程对象

使用 async 定义的协程,协程性质可以总结如下:

使用 async def 声明的函数将总是成为协程,即使它们不包含 await 表达式。该函数将总是返回一个协程对象(coroutine object),类似于生成器和生成器对象。

在 async def 声明的函数中使用 yield 或者 yield from 表达式将得到SyntaxError。

原生协程内部基于生成器实现,也有 send()、throw() 和 close() 方法。

如果协程内部引发了 StopIteration 异常,那么该异常将不会传播给调用者,而是传播 RuntimeError。这对于普通生成器也成立。

当原生协程被垃圾回收时,如果它从来没有 await on,那么则会产生Runtimewarning。

兼容原生协程和基于生成器的协程

在引入 async 定义原生协程的同时,Python 在 C 代码层还引入了两个标志:

引入 CO_COROUTINE 来标记原生协程

引入 CO_ITERABLE_COROUTINE 标志使得基于生成器的协程能够和原生协程兼容

CO_ITERABLE_COROUTINE 标志将被函数 types.coroutine(fn) 使用。types.coroutine 是个装饰器,具体用法如下:

>>>import types

>>>@types.coroutine

...defprocess_data(db):

...data = yield from read_data(db) # read_data 定义在前面

...

>>>p = process_data(None)

>>>import inspect

>>>inspect.isgenerator(p)

True

>>>import asyncio

>>>asyncio.iscoroutine(p)

True

函数 type.coroutine 将会对基于生成器的协程设置 CO_ITERABLE_COROUTINE 标志, 使得其返回一个协程对象(但是其类型仍是 generator)。之所以让其返回一个协程对象,是为了与 await 表达式兼容,详情见下面。

Await 表达式

await 表达式是用来获得一个协程执行的结果,用法如下:

>>>async defread_data(db):

...data = await data.fetch('SELECT ...')

...

类似于 yield from 表达式,await 将会暂停协程 read_data 的执行直到 db.fetch 完成执行并返回结果,这时 read_data 才能继续执行。事实上,await 表达式使用了 yield from 的实现,只是多了一步参数检查,其只接受一个 awaitable 对象,否则会产生 TypeError。awaitable 对象可以是下面的几种:

由原生协程函数返回的原生协程对象

由 types.coroutine() 装饰的函数返回的基于生成器的协程对象

实现了 __await__ 方法并返回一个迭代器的对象,这样的对象又称为 Future-like 对象

使用 CPython C API 定义的对象,实现了 tp_as_async.am_await 函数,返回一个迭代器(类似于 、__await__ 方法)

await 表达式只能在原生协程(使用 async def定义)中使用,否则会产生SyntaxError(类似于 yield 只能 def 函数定义中使用)

Python 调整了 await 表达式的优先级,使得其低于 [],(),和. ,但是高于 ** 运算符,这样大部分情况下就不需要给 await 表达式加上括号了。这里列举了一些合法的和非法的 await 表达式。

原生协程和基于生成器的协程的区别

原生协程没有实现 __iter__ 和 __next__ 方法。所以,它们无法使用 for…in 循环迭代,也不能对它们调用 iter()、list()、tuple() 函数。这么做是为了区分协程和生成器。

普通的生成器不能 yield from 原生协程,否则会产生 TypeError,但是使用 @types.coroutine 或者 @asyncio.coroutine 装饰之后的生成器可以 yield from 原生协程对象

对原生协程函数和原生协程对象,使用 insepct.isgenerator() 和 inspect.isgeneratorfunction() 将返回 False

异步上下文管理器和 “async with”

得益于 async 语法的支持,Python 现在支持异步上下文管理器。一个异步上下文管理器既是上下文管理器,又能够在其 enter 或者 exit 时暂停执行。为了达到这个目的,一个新的协议被提出:添加两个魔术方法 __aenter__ 和 __aexit__。这两个方法都返回一个 awaitable 对象。异步上下文管理器使用方法如下:

async with EXPR as VAR:

BLOCK

上述代码语义上等价于:

mgr = (EXPR) # 初始化

aexit = type(mgr).__aexit__

aenter = type(mgr).__aenter__(mgr) # 执行 __aenter__ 方法,将返回的 awaitable 对象赋值给 aenter

exc = True

VAR = await aenter # 等待 aenter 完成执行并将返回值赋值给 VAR

try:

BLOCK

except:

if not await aexit(mgr,*sys.exc_info()): # aexit 等价于 __aexit__

raise

else:

await aexit(mgr,None,None,None)

aysnc with 只允许在 async def 函数中使用,否则会产生 SyntaxError。

异步迭代器和 “async for”

一个异步可迭代对象(asynchronous iterable)是能在其 iter 方法实现中调用异步代码,一个异步迭代器(asynchronous iterator)是能在其 next 方法中调用异步代码。一个对象要能支持异步迭代:

必须实现 __aiter__ 方法(或者,使用 CPython C API 定义的话,要实现 tp_as_async.am_aiter 槽)返回一个异步迭代器对象

异步迭代器对象必须实现 __anext__ 方法(或者,使用 CPython C API 定义的话,要实现 ty_as_async.am_anext 槽)返回一个 awaitable 对象

为了停止迭代,__anext__ 必须引发 StopAsyncIteration 异常

异步迭代器使用语法如下:

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) # 将 awaitable 对象返回的值赋值给 TARGET

except StopAsyncIteration:

running = False

else:

BLOCK

else:

BLOCK2

与 async with 类似,async for 也只能在 async def 函数中使用。

下面的例子展示了新的异步迭代协议:

classCursor:

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)

注意事项

如果使用了 async 定义协程,这时只能在函数体中使用 await 表达式,而不能使用 yield 或者 yield from,否则会产生 SyntaxError

await 表达式只接受 awaitable 对象,其它值将会产生 TypeError

使用 async 定义的原生协程不能直接使用,需要用到事件循环(event loop),可以使用 asyncio 库

为了保持兼容,需要将 @asyncio.coroutine 替换成 types.coroutine() 函数

原生协程与基于生成器的协程性能基本相近

async 和 await 目前只是语法,还没有成为关键字,需要等到 Python 3.7,才能正式成为关键字

总结

async 为定义协程提供了更清晰的语法,也有助于我们区分协程和生成器。async 需要配合 await 表达式使用。await 表达式只接受 awaitable 对象,将会暂停当前协程的执行,等待 awaitable 对象完成执行并返回结果,其效果类似于 yield from。

Logo

一站式 AI 云服务平台

更多推荐