初探 Python async 和 await

概要

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

  1. Python 原来的协程是通过生成器实现的,所以它们两者拥有相同的语法,详见 PEP 342 – Coroutines via Enhanced Generators 和 PEP 380 – Syntax for Delegating to a Subgenerator。虽然生成器和协程是两个不同的概念,但是由于 Python 通过生成器实现协程,所以很容易认为生成器和协程是相同的。通过引入 async ,我们可以更好的区分生成器和协程,消除了二义性。
  2. 异步调用受限于 yield 表达式能出现的地方,即我们只能实现异步函数。通过引入 async ,我们可以定义异步 for 循环和异步上下文管理器。
  3. 一个函数是否是协程取决于函数体中是否有 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 表达式

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

  1. 原生协程没有实现 __iter__ 和 __next__ 方法。所以,它们无法使用 for…in 循环迭代,也不能对它们调用 iter()、list()、tuple() 函数。这么做是为了区分协程和生成器。
  2. 普通的生成器不能 yield from 原生协程,否则会产生 TypeError,但是使用 @types.coroutine 或者 @asyncio.coroutine 装饰之后的生成器可以 yield from 原生协程对象
  3. 对原生协程函数和原生协程对象,使用 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 方法中调用异步代码。一个对象要能支持异步迭代:

  1. 必须实现 __aiter__ 方法(或者,使用 CPython C API 定义的话,要实现 tp_as_async.am_aiter 槽)返回一个异步迭代器对象
  2. 异步迭代器对象必须实现 __anext__ 方法(或者,使用 CPython C API 定义的话,要实现 ty_as_async.am_anext 槽)返回一个 awaitable 对象
  3. 为了停止迭代,__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)

注意事项

  1. 如果使用了 async 定义协程,这时只能在函数体中使用 await 表达式,而不能使用 yield 或者 yield from,否则会产生 SyntaxError
  2. await 表达式只接受 awaitable 对象,其它值将会产生 TypeError
  3. 使用 async 定义的原生协程不能直接使用,需要用到事件循环(event loop),可以使用 asyncio 库
  4. 为了保持兼容,需要将 @asyncio.coroutine 替换成 types.coroutine() 函数
  5. 原生协程与基于生成器的协程性能基本相近
  6. 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 装饰器入门

posted @ 2017-07-26 14:42  天涯海角路  阅读(894)  评论(0编辑  收藏  举报