Python PEP 492 中文翻譯——協程與async/await語法


原文標題: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.html

用幾句話說明這個PEP:

  1. 把協程的概念從生成器獨立出來,並為之添加了新語句(async/await)。
  2. 但是在CPython的內部實現,協程仍然是一個生成器。
  3. 增加了異步迭代器(async for),異步迭代器的__aiter__、__anext__函數是協程,可以將程序掛起。
  4. 增加了異步上下文管理器(async with),異步上下文管理器的__aenter__、__aexit__函數是協程,可以將程序掛起。

PEP 492: 協程與async/await語法

>摘要

不斷增多的Internet連接程序刺激了對響應性、伸縮性代碼的需求。這個PEP的目標在於:制訂顯式的異步/並發語法,比傳統的Python方法更易用、更豐富。

我們准備把協程(協同程序)的概念獨立出來,並為其使用新的語法。最終目標是建立一個通用、易學的異步編程的構思模型,並盡量與同步編程的風格相似。

這個PEP假設異步任務被一個事件循環器(類似於標准庫里的 asyncio.events.AbstractEventLoop)管理和調度。然而我們並不會依賴某個事件循環器的具體實現方法,從本質上說只與此相關:采用yield作為給調度器的信號,表示協程將會掛起、等待一個異步事件(如IO)的完成。

在這個異步編程不斷增長的時期,我們相信這些改變將會使Python保持一定的競爭性,就像許多其它編程語言已經、將要進行的改變那樣。

>API設計和實現的備注

根據Python 3.5 Beta期間的反饋,進行了重新設計,明確地把協程從生成器里獨立出來了。協程現在是原生的,有明確的獨立類型,而不是作為生成器的一種特殊形式。

這個改變,主要是為了解決在Tornado里使用協程出現的一些問題。
【譯注:在Tornado 4.3已經可以使用新的async/await語句,詳見此鏈接

>理論和目標

在以前,我們可以用生成器實現協程(PEP 342),后來又對其進行了改進,引入了yield from語法(PEP 380)。但仍有一些缺點:

  • 協程和普通生成器使用相同的語法,所以很容易把它們搞混,初學者更是如此。
  • 一個函數是否是一個協程,取決於它里面是否出現了yield或yield from語句。這並不明顯,容易在重構函數的時候搞亂,導致出錯。
  • 異步調用被yield語法限制了,我們不能獲得、使用更多的語法特性,比如with和for。

這個PEP把協程從生成器獨立出來,成為Python的一個原生事物。這會消除協程和生成器之間的混淆,方便編寫不依賴特定庫的協程代碼。也為linter和IDE進行代碼靜態分析提供了機會。
【譯注:在CPython內部,原生協程仍然是基於生成器實現的。】

使用原生協程和相應的新語法,我們可以在異步編程時使用上下文管理器(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

協程語法的關鍵點:

  • async def函數必定是協程,即使里面不含有await語句。
  • 如果在async函數里面使用yield或yield from語句,會引發SyntaxError異常。
  • 在CPython內部,引入兩個新的代碼對象標識(code object flags):
    1, CO_COROUTINE表示這是原生協程。(由新語法定義)
    2, CO_ITERABLE_COROUTINE表示這是用生成器實現的協程,但是和原生協程兼容。(用裝飾器types.coroutine()裝飾過的生成器協程)
  • 調用一個普通生成器,返回一個生成器對象(generator object);相應的,調用一個協程返回一個協程對象(coroutine object)。
  • 協程不再拋出StopIteration異常,因為拋出的StopIteration異常會被包裝(wrap)成一個RuntimeError異常。(在Python 3.5,對於普通生成器要想這樣需要進行future import,見PEP 479)。
  • 如果一個協程從未await等待就被垃圾收集器銷毀了,會引發一個RuntimeWarning異常(見“調試特性”)。
  • 更多請參考“協程對象”一節。

>>types.coroutine()

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表達式

新的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對象是以下的其中一個:

  • 一個原生協程對象(由一個原生協程函數返回)

  • 用裝飾器types.coroutine()裝飾的一個“生成器實現的協程”對象

  • 一個有__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語句多數情況下不需要被圓括號包圍。

>>>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())

>>異步上下文管理器和“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)
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):
    ...

>>異步迭代器和“async for”

異步迭代器可以在它的iter實現里掛起、調用異步代碼,也可以在它的__next__方法里掛起、調用異步代碼。要支持異步迭代,需要:

  1. 對象必須實現一個__aiter__方法(或者,如果使用CPython C API,需要定義tp_as_async.am_aiter),返回一個異步迭代器對象,這個異步迭代器對象在每次迭代時會返回一個awaitable。
  2. 一個異步迭代器必須實現一個__anext__方法(或者,如果使用CPython C API,需要定義tp_as_async.am_anext),在每次迭代時返回一個awaitable。
  3. 要停止迭代,__anext__必須拋出一個StopAsyncIteration異常。

異步迭代的一個示例:

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分句。

>>>示例1

有了異步迭代,我們可以在迭代時異步緩沖(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)

>>>示例2

這是一個便利類,用於把普通的迭代對象轉變為一個異步迭代對象。雖然這個類沒什么實際用處,但它演示了普通迭代器和異步迭代器的關系:

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)

>>>為什么是StopAsyncIteration?

在CPython內部,協程的實現仍然是基於生成器的。所以,在PEP 479生效之前【譯注:將在Python 3.7正式生效,在3.5、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模塊里現有的“基於生成器的協程”,仍然保持不變。

為了在概念上把協程和生成器區分開來,做了以下規定:

  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。

【譯注: @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 342、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。

>>新的標准庫函數

  • 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)和其它編譯器(如Cython)相整合,增加了兩個新的抽象基類(Abstract Base Classes):

  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())

為了更容易地對異步迭代進行調試,又增加了兩個抽象基類:

  1. collections.abc.AsyncIterable --用於測試__aiter__方法。
  2. collections.abc.AsyncIterator --用於測試__aiter__和__anext__方法。

>詞匯表

原生協程函數 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

asyncio模塊已經可以使用新語法,並經過測試,100%與async/await兼容。現有的使用asyncio的代碼在使用新語法時可以保持不變。
為此,對asyncio模塊主要做了如下修改:

  1. 在 @asyncio.coroutine裝飾器內部,調用types.coroutine為函數設置一個CO_ITERABLE_COROUTINE標識。
  2. 給asyncio.Future類添加一行代碼: __await__ = __iter__。
  3. 把async()函數改名為ensure_future(),以防該函數名和新關鍵字沖突。

>>>asyncio遷移策略

由於未經裝飾的生成器不能yield from原生協程對象(詳見“和生成器的不同之處”一節),因此在使用新語法前,請確保所有“基於生成器的協程”都被 @asyncio.coroutine裝飾器裝飾。

>>啟用關鍵字的計划

async和await在CPython 3.5、3.6里暫時不是正式的關鍵字,在CPython 3.7它們將變成正式的關鍵字。如果不這樣,恐怕對現有代碼的遷移造成困難。
【譯注:在某些現有代碼里,可能使用了async和await作為變量名/函數名。然而Python不允許把關鍵字當作變量名/函數名,所以3.5、3.6給程序員留了一些遷移時間。】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM