欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

PEP 492 -- Coroutines with async and await syntax 翻译

程序员文章站 2023-10-30 21:57:58
因为工作中慢慢开始用python的协程,所以想更好的理解一下实现方式,故翻译此文 原文中把词汇表放到最后,但是我个人觉得放在最开始比较好,这样可以增加当你看原文时的理解程度 词汇表 原生协程函数 Native coroutine function: 由async def定义的协程函数,可以使用awa ......

因为工作中慢慢开始用python的协程,所以想更好的理解一下实现方式,故翻译此文

原文中把词汇表放到最后,但是我个人觉得放在最开始比较好,这样可以增加当你看原文时的理解程度

词汇表

原生协程函数 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’”一节。

摘要

随着互联网和连接程序的增长,引发了对响应性和可扩展代码的需求,该提议的目标是让我们共容易的通过编写显示异步,高并发的python代码并且更加pythonic

它提出把写成的概念独立出来,并引入新的支持语法。最终的目标是帮助在python中建立一个通用的,易于接近的异步编程构思模型,并使其尽可能接近于同步编程(说白了就是让你通过类似写同步编程的方式,写出异步代码)

 

这个pepe建设异步任务是类似于标准模块asyncio.events.abstracteventloop的事件循环调度和协调。虽然这个pep不依赖人去特定的时间循环实现,但它仅仅与使用yield作为调度程序信号的协程类型相关,表示协程将等待知道事件(例如:io)完成

我们相信,这里提出的更改将有助于python在快速增长的异步编程领域保持更好的竞争力,因为许多其他语言已经采或将要采用类似的功能

api设计和实施修订

对python 3.5的初始beta版本的反馈导致重新设计支持此pep的对象模型,以更清楚地将原生协程与生成器分离 - 而不是一种新的生成器,现在原生协程有明确的独立类型

这个改变主要是为了解决原生协程在tornado里使用出现的一些问题

 

在cpython3.5.2 中更新了__aiter__ 协议。

在3.5.2之前,__aiter__ 是被期望返回一个等待解析为异步迭代器,从3.5.2开始,__aiter__ 应该直接返回异步迭代器

如果在3.5.2中使用旧协议中,python将引发pendingdeprecationwarning异常

在cpython 3.6中,旧的__aiter__协议仍将受到引发deprecationwarning的支持

在cpython 3.7中,将不再支持旧的__aiter__协议:如果__aiter__返回除异步迭代器之外的任何内容,则将引发runtimeerror。

 

理论和目标

当前的python支持通过生成器(pep342)实现协程,并通过pep380中引入的yield from 语法进一步增强,这种方法有很多缺点:

  • 协程序与生成器具有相同的语法,很容易混淆,对于初级开发者来说尤其如此。
  • 一个函数是否是一个协程,取决于它里面是否出现了yield或yield from语句。这并不明显,容易在重构函数的时候搞乱,导致出错。
  • 异步调用被yield语法限制了,我们不能获得、使用更多的语法特性,比如with和for。

这个pep把协程从生成器独立出来,成为python的一个原生事物。这会消除协程和生成器之间的混淆,方便编写不依赖特定库的协程代码。也为linter和ide进行代码静态分析提供了机会。

使用原生协程和相应的新语法,我们可以在异步编程时使用上下文管理器(context manager)和迭代器。如下文所示,新的async with语句可以在进入、离开运行上下文(runtime context)时进行异步调用,而async for语句可以在迭代时进行异步调用。

 

规范

该提议引入了新的语法和语义来增强python对协程支持。

请理解python现有的协程(见pep 342和pep 380),这次改变的动机来自于asyncio框架(pep 3156)和confunctions提案(pep 3152,此pep已经被废弃)。

由此,在本文中,我们使用“原生协程”指用新语法声明的协程。“生成器实现的协程”指用传统方法实现的协程。“协程”则用在两个都可以使用的地方。

新的协程声明语法

使用以下语法声明原生协程:

async def read_data(db):
    pass

协程语法的关键点:

  • async def函数必定是协程,即使里面不含有await语句。
  • 如果在async函数里面使用yield或yield from语句,会引发syntaxerror异常。
  • 在cpython内部,引入两个新的代码对象标识(code object flags):
    co_coroutine表示这是原生协程。(由新语法定义)
    co_iterable_coroutine表示这是用生成器实现的协程,但是和原生协程兼容。(用装饰器types.coroutine()装饰过的生成器协程)
  • 调用一个普通生成器,返回一个生成器对象(generator object);相应的,调用一个协程返回一个协程对象(coroutine object
  • 协程不再抛出stopiteration异常,因为抛出的stopiteration异常会被包装(wrap)成一个runtimeerror异常。对于普通的生成器想要这样需要进行future import
  • 如果一个协程从未await等待就被垃圾收集器销毁了,会引发一个runtimewarning异常

types.coroutine()

types模块添加了一个新函数coroutine(fn),使用它,“生成器实现的协程”和“原生协程”之间可以进行互操作。 

@types.coroutine
def process_data(db):
    data = yield from read_data(db)
    ...

该函数将co_iterable_coroutine标志应用于生成器函数的代码对象,使其返回一个协程对象。如果fn不是生成器函数,它将被包装。如果它返回一个生成器,它将被包装在一个等待的代理对象中(参见下面的等待对象的定义)。

types.coroutine()不会设置co_coroutine标识,只有用新语法定义的原生协程才会有这个标识。

await表达式

新的await表达式用于获得协程执行的结果:

async def read_data(db):
    data = await db.fetch('select ...')
    ...

await 和yield from 是非常类似的,会挂起read_data的执行,直到等待db.fetch完成并返回结果数据。

await使用yield from的实现,但是加入了一个额外步骤——验证它的参数类型。await只接受awaitable对象,awaitable对象是以下的其中一个:

  • 一个原生协程对象(由一个原生协程函数返回)
  • 用装饰器types.coroutine()装饰的一个“生成器实现的协程”对象
  • 一个有__await__方法的对象(__await__方法返回的一个迭代器)。调用链上的每一个yield from 最终都会以一个yield结束,这是future实现的基本机制。在python内部,协程是一种特殊的生成器,所以每个await最终会被await调用链条上的某个yield语句挂起。为了让协程也有这样的行为,添加了一个新的魔术方法__await__。

    例如,在asyncio模块,要想在await语句里使用future对象,唯一的修改是给asyncio.future加一行:__await__ = __iter__

在本文中,有__await__方法的对象被称为future-like对象(协程会被await语句挂起,直到await语句右边的future-like对象的__await__执行完毕、返回结果。)

如果__await__返回的不是一个迭代器,则引发typeerror异常。 

在cpython c api,有tp_as_async.am_await函数的对象,该函数返回一个迭代器(类似__await__方法)

如果在async def函数之外使用await语句,会引发syntaxerror异常。这和在def函数之外使用yield语句一样。

如果await右边不是一个awaitable对象,会引发typeerror异常。

 

新的运算符优先级表

有效的语法示例

 

expression will be parsed as
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())

 

 

 

 

 

 

 

 

 

 

 

无效的用法

 

expression should be written as
await await coro() await (await coro())
await -coro() await (-coro())
 

 

 

 

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

 

异步上下文管理器(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)

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):
    ...

异步迭代器和“async for”

异步迭代器可以在它的iter实现里挂起、调用异步代码,也可以在它的__next__方法里挂起、调用异步代码。要支持异步迭代,需要:

  • 对象必须实现__aiter__方法(或者,如果使用cpython c api,需要定义tp_as_async.am_aiter)返回一个异步迭代器对象
  • 一个异步迭代对象必须实现一个__anext__方法(或者,如果使用cpython c api,需要定义tp_as_async.am_anext)返回一个awaitable
  • 要停止迭代,__anext__必须抛出一个stopasynciteration异常。

一个一步迭代的例子:

class asynciterable:
    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 = 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分句。

例子1

使用异步迭代协议,可以在迭代期间异步缓冲数据:

async for data in cursor:
    ...

其中cursor是一个异步迭代器,它在每n次迭代后从数据库中预取n行数据。

以下代码说明了新的异步迭代协议:

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 = await cursor().__aiter__()
while true:
    try:
        row = await i.__anext__()
    except stopasynciteration:
        break
    else:
        print(row)

例子2:

以下是将常规迭代转换为异步迭代的实用程序类。虽然这不是一件非常有用的事情,但代码说明了常规迭代器和异步迭代器之间的关系。

class asynciteratorwrapper:
    def __init__(self, obj):
        self._it = iter(obj)

    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)

为什么是stopasynciteration?

协程在内部仍然是基于生成器实现的,因此,在pep479之前,下面两者是没有区别的

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异常。

协程对象

和生成器的不同之处

本节仅适用于具有co_coroutine的原生协程,即使用新的async def 定义的函数

对于asyncio模块里现有的“基于生成器的协程”,仍然保持不变。

为了把协程和生成器的概念区分开来:

  1. 原生协程对象不实现__iter__和__next__方法,因此,不能对其进行迭代(如for...in循环),也不能传递给iter(),list(),tuple()及其它内置函数。如果尝试对其使用__iter__或__next__方法,会引发typeerror异常。
  2. 未装饰的生成器不能yield from一个原生协程,这样做会引发typeerror异常。
  3. “基于生成器的协程”在经过 @asyncio.coroutine装饰后,可以yield from原生协程对象。
  4. 对于原生协程对象和原生协程函数,调用inspect.isgenerator()和inspect.isgeneratorfunction()会返回false。

协程对象的方法

协程是基于生成器实现的,因此它们有共同的代码。像生成器对象那样,协程也有throw(),send()和close()方法。
对于协程,stopiteration和generatorexit起着同样的作用(虽然pep 479已经应用于协程)。详见pep 342、pep 380,以及python文档。

对于协程,send(),throw()方法用于往future-like对象发送内容、抛出异常。

调试特性

初级开发者在使用协程时可能忘记使用yield from语句,比如:

@asyncio.coroutine
def useful():
    asyncio.sleep(1) # this will do nothing without 'yield from'

 

为了调试这种错误,在asyncio中有一个特殊的调试模式,其中@coroutine装饰器用一个特殊对象包装所有函数,并使用析构函数记录警告。每当一个包装的生成器被垃圾回收时,就会生成一条详细的日志消息,其中包含有关定义装饰器函数的确切位置,堆栈跟踪收集位置等的信息.wrapper对象还提供了一个方便的__repr__函数,其中包含有关生成器的详细信息。

新标准库函数

  • 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会重置(reset)这个wrapper。如果再次调用,新的wrapper会取代旧的。这个函数是线程专有的(thread-specific)。详见“调度特性”一节。
  • sys.get_coroutine_wrapper() 返回当前的包装对象(wrapper object)。如果没有则返回none。这个函数是线程专有的(thread-specific)。详见“调度特性”一节。

新的抽象基类

为了更好地与现有框架(如tornado,见[13])和编译器(如cython,见[16])集成,增加了两个新的抽象基类(abc):

  1. collections.abc.awaitable,future-like类的抽象基类,实现__await__方法。
  2. collections.abc.coroutine,协程对象的抽象基类,实现send(value),throw(type, exc, tb),close()和__await__()方法。

注意,“基于生成器的协程”(有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())

为了便于测试对象是否支持异步迭代,还添加了两个abc:

  1. collections.abc.asynciterable --用于测试__aiter__方法。
  2. collections.abc.asynciterator --用于测试__aiter__和__anext__方法。