初探 Python async 和 await
概要
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 def read_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
... def process_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 def read_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 函数中使用。
下面的例子展示了新的异步迭代协议:
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)
注意事项
- 如果使用了 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。
参考: PEP 492 – Coroutines with async and await syntax
Previous post: 理解 Python 的 yield from
Next post: Python 装饰器入门