Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 492 – 使用 async 與 await 語法的協程 (Coroutines with async and await syntax)

作者:
Yury Selivanov <yury at edgedb.com>
討論於:
Python-Dev 列表
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
建立日期:
2015 年 4 月 9 日
Python 版本:
3.5
公告歷史:
2015 年 4 月 17 日, 2015 年 4 月 21 日, 2015 年 4 月 27 日, 2015 年 4 月 29 日, 2015 年 5 月 5 日

目錄

摘要

網際網路的成長與普及的連線性,觸發了對反應迅速且具擴展性之程式碼的相應需求。本提案旨在透過讓撰寫明確的非同步、並行 Python 程式碼變得更容易且更符合 Python 風格(Pythonic),來回應這項需求。

本提案建議將「協程(coroutines)」作為 Python 中一個適當的獨立概念,並引入新的支援語法。最終目標是幫助建立一個共同且易於理解的 Python 非同步程式設計心理模型,並使其盡可能接近同步程式設計。

本 PEP 假設非同步任務是由事件迴圈(Event Loop)調度與協調的,類似於標準函式庫模組 asyncio.events.AbstractEventLoop。雖然本 PEP 不受限於任何特定的事件迴圈實作,但它僅與那種使用 yield 作為給調度器之訊號、表示協程將等待事件(如 IO)完成的協程相關。

我們相信此處提出的變更將有助於讓 Python 在快速成長的非同步程式設計領域保持相關性與競爭力,因為許多其他語言已經採用或計畫採用類似的功能:[2], [5], [6], [7], [8], [10]

API 設計與實作修訂

  1. 根據對 Python 3.5 最初測試版的回饋,支援本 PEP 的對象模型進行了重新設計,以便更清晰地將原生協程與產生器分開——原生協程不再是一種新型別的產生器,而是擁有自己完全獨特的型別(實作於 [17])。

    這項變更主要是基於在嘗試將原生協程支援整合到 Tornado 網頁伺服器時遇到的問題(詳見 [18])。

  2. 在 CPython 3.5.2 中,__aiter__ 協定進行了更新。

    在 3.5.2 之前,__aiter__ 預期回傳一個可解析為「非同步迭代器」的「可等待對象 (awaitable)」。從 3.5.2 開始,__aiter__ 應直接回傳非同步迭代器。

    如果 3.5.2 中使用了舊協定,Python 將引發 PendingDeprecationWarning

    在 CPython 3.6 中,舊的 __aiter__ 協定仍將受到支援,但會引發 DeprecationWarning

    在 CPython 3.7 中,舊的 __aiter__ 協定將不再受支援:如果 __aiter__ 回傳非同步迭代器以外的任何內容,將引發 RuntimeError

    詳見 [19][20]

原理與目標

目前的 Python 支援透過產生器實作協程(PEP 342),並由 PEP 380 引入的 yield from 語法進一步增強。這種方法有一些缺點:

  • 協程很容易與常規產生器混淆,因為它們共用相同的語法;對於新開發者來說尤其如此。
  • 一個函式是否為協程是由其「本體」中是否存在 yieldyield from 陳述式決定的,這可能導致在重構期間當此類陳述式出現或從函式本體消失時,產生不明顯的錯誤。
  • 對非同步呼叫的支援僅限於語法上允許使用 yield 的表達式,這限制了 withfor 等語法功能的實用性。

本提案將協程作為一種原生的 Python 語言功能,並將其與產生器明確分開。這消除了產生器與協程間的歧義,並使得在不依賴特定函式庫的情況下可靠地定義協程成為可能。這也使 Linter 與 IDE 能夠改進靜態程式碼分析與重構。

原生協程及相關的新語法功能使得以非同步術語定義上下文管理器與迭代器協定成為可能。如本提案後文所示,新的 async with 陳述式讓 Python 程式在進入與退出執行階段上下文時能執行非同步呼叫,而新的 async for 陳述式則使迭代器中執行非同步呼叫成為可能。

規範

本提案引入了新的語法與語義,以增強 Python 中的協程支援。

本規範假定讀者具備 Python 中協程實作的知識(PEP 342PEP 380)。此處提出的語法變更動力來自 asyncio 框架(PEP 3156)與「Cofunctions」提案(PEP 3152,現已被拒絕,改採本規範)。

從本文件此處起,我們使用「原生協程 (native coroutine)」一詞指代使用新語法宣告的函式。必要時使用「基於產生器的協程 (generator-based coroutine)」指代基於產生器語法的協程。在兩種定義皆適用的情況下使用「協程 (coroutine)」。

新的協程宣告語法

以下新語法用於宣告「原生協程」:

async def read_data(db):
    pass

「協程」的關鍵特性:

  • async def 函式永遠是協程,即使它們不包含 await 表達式。
  • async 函式中包含 yieldyield from 表達式會導致 SyntaxError
  • 在內部,引入了兩個新的程式碼對象旗標(code object flags):
    • CO_COROUTINE 用於標記「原生協程」(使用新語法定義)。
    • CO_ITERABLE_COROUTINE 用於使「基於產生器的協程」與「原生協程」相容(由 types.coroutine() 函式設定)。
  • 常規產生器在呼叫時會回傳「產生器對象」;同樣地,協程會回傳「協程」對象。
  • StopIteration 異常不會傳播到協程之外,並會被替換為 RuntimeError。對於常規產生器,此類行為需要 future 導入(參見 PEP 479)。
  • 當「原生協程」被垃圾回收時,如果它從未被等待(await)過,則會引發 RuntimeWarning(另見 偵錯功能)。
  • 另見 協程對象 章節。

types.coroutine()

types 模組中新增了一個函式 coroutine(fn)。它允許 asyncio 中現有的「基於產生器的協程」與本 PEP 引入的「原生協程」之間進行互操作:

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

該函式將 CO_ITERABLE_COROUTINE 旗標應用於產生器函式的程式碼對象,使其回傳「協程」對象。

如果 fn 不是「產生器函式」,它會被封裝。如果它回傳一個「產生器」,則會被封裝在一個「可等待(awaitable)」代理對象中(見下文可等待對象的定義)。

請注意,types.coroutine() 不會應用 CO_COROUTINE 旗標,以便區分使用新語法定義的「原生協程」與「基於產生器的協程」。

Await 表達式

以下新的 await 表達式用於獲取協程執行的結果:

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

awaityield from 類似,會暫停 read_data 協程的執行,直到 db.fetch 這個「可等待對象」完成並回傳結果數據。

它使用 yield from 的實作,並額外增加了一個驗證其參數的步驟。await 僅接受「可等待對象 (awaitable)」,其可以是以下之一:

  • 從「原生協程函式」回傳的「原生協程」對象。
  • 從裝飾了 types.coroutine() 的函式回傳的「基於產生器的協程」對象。
  • 具有回傳迭代器的 __await__ 方法的對象。

    任何 yield from 呼叫鏈最終都會以一個 yield 結束。這是「Future」實作的基本機制。由於在內部協程是一種特殊的產生器,因此每個 await 都會被 await 呼叫鏈下游某處的 yield 暫停(詳細說明請參考 PEP 3156)。

    為了給協程開啟此行為,新增了一個名為 __await__ 的魔術方法。例如在 asyncio 中,要在 await 陳述式中啟用 Future 對象,唯一的變更是將 __await__ = __iter__ 這一行加入 asyncio.Future 類別中。

    在本 PEP 的其餘部分,具有 __await__ 方法的對象被稱為「類 Future (Future-like)」對象。

    如果 __await__ 回傳的不是迭代器,則會引發 TypeError

  • 使用 CPython C API 定義且具有 tp_as_async.am_await 函式(回傳一個迭代器,類似於 __await__ 方法)的對象。

async def 函式之外使用 await 會導致 SyntaxError(就像在 def 函式之外使用 yieldSyntaxError 一樣)。

將「可等待對象」以外的任何東西傳遞給 await 表達式會導致 TypeError

更新的運算子優先級表

await 關鍵字定義如下:

power ::=  await ["**" u_expr]
await ::=  ["await"] primary

其中「primary」代表語言中結合最緊密的運算。其語法為:

primary ::=  atom | attributeref | subscription | slicing | call

詳見 Python 文件 [12] 與本提案的 語法更新 章節。

awaityieldyield from 運算子的關鍵區別在於,「await 表達式」在大多數情況下不需要括號封裝。

此外,yield from 允許任何表達式作為其參數,包括像 yield from a() + b() 這樣的表達式,它會被解析為 yield from (a() + b()),這幾乎總是一個錯誤。通常情況下,任何算術運算的結果都不是「可等待對象」。為了避免這種錯誤,決定讓 await 的優先級低於 []().,但高於 ** 運算子。

運算子 描述
yield x, yield from x Yield 表達式
lambda Lambda 表達式
ifelse 條件表達式
or 布林 OR
and 布林 AND
not x 布林 NOT
in, not in, is, is not, <, <=, >, >=, !=, == 比較,包括成員測試與身分測試
| 位元 OR
^ 位元 XOR
& 位元 AND
<<, >> 位元移位
+, - 加法與減法
*, @, /, //, % 乘法、矩陣乘法、除法、餘數
+x, -x, ~x 正號、負號、位元 NOT
** 指數運算
await x Await 表達式
x[index], x[index:index], x(arguments...), x.attribute 索引取值、切片、呼叫、屬性引用
(expressions...), [expressions...], {key: value...}, {expressions...} 綁定或 Tuple 展示、List 展示、Dict 展示、Set 展示

「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__。兩者都必須回傳一個「可等待對象」。

非同步上下文管理器範例:

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__

VAR = await aenter(mgr)
try:
    BLOCK
except:
    if not await aexit(mgr, *sys.exc_info()):
        raise
else:
    await aexit(mgr, None, None, None)

與常規 with 陳述式一樣,可以在單個 async with 陳述式中指定多個上下文管理器。

將沒有 __aenter____aexit__ 方法的常規上下文管理器傳遞給 async with 會報錯。在 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」

「非同步可迭代對象 (asynchronous iterable)」能夠在其「iter」實作中呼叫非同步程式碼,而「非同步迭代器 (asynchronous iterator)」可以在其「next」方法中呼叫非同步程式碼。為了支援非同步迭代:

  1. 對象必須實作一個 __aiter__ 方法(或者如果是使用 CPython C API 定義的,則為 tp_as_async.am_aiter 插槽),回傳一個「非同步迭代器對象」。
  2. 「非同步迭代器對象」必須實作一個 __anext__ 方法(或者如果是使用 CPython C API 定義的,則為 tp_as_async.am_anext 插槽),回傳一個「可等待對象」。
  3. 要停止迭代,__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

將沒有 __aiter__ 方法的常規可迭代對象傳遞給 async for 會導致 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 = 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?

協程在內部仍基於產生器。因此,在 PEP 479 之前,以下兩者之間沒有本質區別:

def g1():
    yield from fut
    return 'spam'

and

def g2():
    yield from fut
    raise StopIteration('spam')

由於 PEP 479 已被接受並對協程預設啟用,以下範例將其 StopIteration 封裝為 RuntimeError

async def a1():
    await fut
    raise StopIteration('spam')

告知外部程式碼迭代已結束的唯一方法是引發 StopIteration 以外的異常。因此,新增了一個新的內建異常類別 StopAsyncIteration

此外,根據 PEP 479 的語義,協程中引發的所有 StopIteration 異常都會被封裝在 RuntimeError 中。

協程對象

與產生器的差異

本節僅適用於具有 CO_COROUTINE 旗標的「原生協程」,即使用新 async def 語法定義的協程。

asyncio 中現有的「基於產生器的協程」之行為保持不變。

我們付出了巨大努力來確保協程與產生器被視為不同的概念:

  1. 「原生協程」對象不實作 __iter____next__ 方法。因此,它們不能被迭代,也不能傳遞給 iter()list()tuple() 等其他內建函式。它們也不能用於 for..in 迴圈。

    嘗試在「原生協程」對象上使用 __iter____next__ 會導致 TypeError

  2. 「純產生器 (Plain generators)」不能對「原生協程」執行 yield from:這樣做會導致 TypeError
  3. 「基於產生器的協程」(對於 asyncio,程式碼必須裝飾為 @asyncio.coroutine [1])可以對「原生協程對象」執行 yield from
  4. inspect.isgenerator()inspect.isgeneratorfunction() 對於「原生協程」對象與「原生協程函式」會回傳 False

協程對象方法

協程在內部基於產生器,因此它們共用實作。與產生器對象類似,「協程」具有 throw()send()close() 方法。StopIterationGeneratorExit 對於協程扮演相同的角色(儘管協程預設啟用了 PEP 479)。詳見 PEP 342PEP 380 以及 Python 文件 [11]

協程的 throw()send() 方法用於將值推入或在「類 Future」對象中引發錯誤。

偵錯功能

初學者常犯的一個錯誤是忘記在協程上使用 yield from

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

為了偵錯這類錯誤,asyncio 中有一種特殊的偵錯模式,在此模式下,@coroutine 裝飾器會將所有函式封裝在一個特殊對象中,該對象具有會記錄警告的解構子(destructor)。每當封裝的產生器被垃圾回收時,會生成一條詳細的日誌訊息,其中包含裝飾器函式定義的確切位置、回收位置的堆疊追蹤等資訊。封裝對象還提供了一個方便的 __repr__ 函式,包含有關產生器的詳細資訊。

唯一的議題是如何啟用這些偵錯功能。由於偵錯設施在生產模式下應該是不產生開銷的(no-op),@coroutine 裝飾器會根據作業系統環境變數 PYTHONASYNCIODEBUG 來決定是否封裝。透過這種方式,可以使用 asyncio 自己的函式檢測來運行 asyncio 程式。另一個偵錯設施 EventLoop.set_debug 則對 @coroutine 裝飾器的行為沒有影響。

根據本提案,協程是一個原生且與產生器區分的概念。「除了」在從未被等待的協程上引發 RuntimeWarning 之外,還提議在 sys 模組中新增兩個函式:set_coroutine_wrapperget_coroutine_wrapper。這是為了在 asyncio 與其他框架中啟用進階偵錯設施(例如顯示協程建立的確切位置,以及垃圾回收處更詳細的堆疊追蹤)。

新的標準函式庫函式

  • 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 必須是一個接受一個參數(「協程」對象)的呼叫對象,或者是 NoneNone 會重設封裝器。如果呼叫兩次,新的封裝器會替換舊的。此函式是執行緒特定的。詳見 偵錯功能
  • sys.get_coroutine_wrapper() 回傳目前的封裝器對象。如果未設定封裝器,則回傳 None。此函式是執行緒特定的。詳見 偵錯功能

新的抽象基底類別 (ABC)

為了能與現有框架(如 Tornado,見 [13])與編譯器(如 Cython,見 [16])更好地整合,新增了兩個抽象基底類別 (ABC):

  • collections.abc.Awaitable:用於實作 __await__ 方法的「類 Future」類別。
  • collections.abc.Coroutine:用於實作 send(value)throw(type, exc, tb)close()__await__() 方法的「協程」對象。

    請注意,帶有 CO_ITERABLE_COROUTINE 旗標的「基於產生器的協程」不實作 __await__ 方法,因此它們不是 collections.abc.Coroutinecollections.abc.Awaitable ABC 的實例:

    @types.coroutine
    def gencoro():
        yield
    
    assert not isinstance(gencoro(), collections.abc.Coroutine)
    
    # however:
    assert inspect.isawaitable(gencoro())
    

為了方便測試對象是否支援非同步迭代,還新增了兩個 ABC:

  • collections.abc.AsyncIterable – 測試 __aiter__ 方法。
  • collections.abc.AsyncIterator – 測試 __aiter____anext__ 方法。

詞彙表

原生協程函式 (Native coroutine function)
使用 async def 宣告的協程函式。它使用 awaitreturn value;詳見 新的協程宣告語法
原生協程 (Native coroutine)
從原生協程函式回傳的對象。詳見 Await 表達式
基於產生器的協程函式 (Generator-based coroutine function)
基於產生器語法的協程。最常見的範例是使用 @asyncio.coroutine 裝飾的函式。
基於產生器的協程 (Generator-based coroutine)
從基於產生器的協程函式回傳的對象。
協程 (Coroutine)
「原生協程」或「基於產生器的協程」。
協程對象 (Coroutine object)
「原生協程對象」或「基於產生器的協程對象」。
類 Future 對象 (Future-like object)
具有 __await__ 方法的對象,或具有 tp_as_async->am_await 函式且回傳「迭代器」的 C 對象。可以在協程中由 await 表達式消耗。等待類 Future 對象的協程會暫停,直到類 Future 對象的 __await__ 完成並回傳結果。詳見 Await 表達式
Awaitable
一個「類 Future 對象」或「協程對象」。詳見 Await 表達式
非同步上下文管理器 (Asynchronous context manager)
具有 __aenter____aexit__ 方法且可與 async with 一起使用的對象。詳見 非同步上下文管理器與「async with」
非同步可迭代對象 (Asynchronous iterable)
具有 __aiter__ 方法且必須回傳「非同步迭代器」對象的對象。可與 async for 一起使用。詳見 非同步迭代器與「async for」
非同步迭代器 (Asynchronous iterator)
具有 __anext__ 方法的對象。詳見 非同步迭代器與「async for」

過渡計畫

為了避免與 asyncawait 關鍵字產生回溯相容性問題,決定以如下方式修改 tokenizer.c

  • 識別 async def NAME 標記組合;
  • 在解析 async def 區塊標記時,將 'async' NAME 標記替換為 ASYNC,將 'await' NAME 標記替換為 AWAIT
  • 在解析 def 區塊標記時,按原樣產出 'async''await' NAME 標記。

這種方法允許將新語法功能(僅在 async 函式中可用)與任何現有程式碼無縫結合。

在一段程式碼中同時擁有「async def」與「async」屬性的範例:

class Spam:
    async = 42

async def ham():
    print(getattr(Spam, 'async'))

# The coroutine can be executed and will print '42'

回溯相容性

本提案保留了 100% 的回溯相容性。

asyncio

asyncio 模組已適配並測試可與協程及新陳述式搭配使用。回溯相容性已 100% 保留,即所有現有程式碼都將按原樣運行。

所需的變更主要包括:

  1. 修改 @asyncio.coroutine 裝飾器以使用新的 types.coroutine() 函式。
  2. asyncio.Future 類別中加入 __await__ = __iter__ 這一行。
  3. 新增 ensure_future() 作為 async() 函式的別名。棄用 async() 函式。

asyncio 遷移策略

由於「純產生器」不能對「原生協程對象」執行 yield from(詳見 與產生器的差異 章節),建議在開始使用新語法「之前」,確保所有基於產生器的協程都已裝飾為 @asyncio.coroutine

CPython 程式碼庫中的 async/await

CPython 中沒有使用 await 名稱的情況。

async 主要由 asyncio 使用。我們正在透過將 async() 函式重新命名為 ensure_future() 來解決此問題(詳見 asyncio 章節)。

async 關鍵字的另一處使用是在 Lib/xml/dom/xmlbuilder.py 中,為 DocumentLS 類別定義 async = False 屬性。對此沒有相關文件或測試,CPython 其他地方也沒有使用。它已被替換為一個 getter,該 getter 會引發 DeprecationWarning,建議改用 async_ 屬性。「async」屬性未見載於文件且未在 CPython 程式碼庫中使用。

語法更新

語法變更非常微小:

decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef

compound_stmt: (if_stmt | while_stmt | for_stmt | try_stmt | with_stmt
                | funcdef | classdef | decorated | async_stmt)

async_stmt: ASYNC (funcdef | with_stmt | for_stmt)

power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*

棄用計畫

asyncawait 名稱將在 CPython 3.5 與 3.6 中進行軟性棄用。在 3.7 中,我們將把它們轉換為正式關鍵字。在 3.7 之前將 asyncawait 作為正式關鍵字,可能會讓使用者更難將其程式碼遷移到 Python 3。

設計考量

PEP 3152

Gregory Ewing 提出的 PEP 3152 提議了一種不同的協程機制(稱為「cofunctions」)。一些關鍵點:

  1. 一個新的關鍵字 codef 用於宣告「cofunction」。Cofunction 永遠是產生器,即使其中沒有 cocall 表達式。對應於本提案中的 async def
  2. 一個新的關鍵字 cocall 用於呼叫「cofunction」。只能在 cofunction 內部使用。對應於本提案中的 await(有一些差異,見下文)。
  3. 若不使用 cocall 關鍵字,就不可能呼叫 cofunction。
  4. cocall 在語法上要求其後跟隨括號:
    atom: cocall | <existing alternatives for atom>
    cocall: 'cocall' atom cotrailer* '(' [arglist] ')'
    cotrailer: '[' subscriptlist ']' | '.' NAME
    
  5. cocall f(*args, **kwds) 在語義上等同於 yield from f.__cocall__(*args, **kwds)

與本提案的差異:

  1. 本 PEP 中沒有與 __cocall__ 等效的東西(即被呼叫且其結果在 cocall 表達式中被傳遞給 yield from)。await 關鍵字預期一個「可等待對象」,驗證其型別,並對其執行 yield from。雖然 __await__ 方法與 __cocall__ 類似,但它僅用於定義「類 Future 對象」。
  2. await 在語法中的定義方式幾乎與 yield from 相同(稍後會強制要求 await 只能在 async def 內部使用)。可以直接寫 await future,而 cocall 總是要求括號。
  3. 要使 asyncio 與 PEP 3152 配合工作,需要修改 @asyncio.coroutine 裝飾器以將所有函式封裝在具有 __cocall__ 方法的對象中,或者在產生器上實作 __cocall__。要從現有的基於產生器的協程呼叫 cofunction,需要使用 costart(cofunc, *args, **kwargs) 內建函式。
  4. 由於不可能在沒有 cocall 關鍵字的情況下呼叫 cofunction,它會自動防止忘記在基於產生器的協程上使用 yield from 的常見錯誤。本提案透過不同的方式解決此問題,請參見 偵錯功能
  5. 要求使用 cocall 關鍵字呼叫協程的一個缺點是,如果決定實作「協程產生器」——帶有 yieldasync yield 表達式的協程——我們不需要 cocall 關鍵字來呼叫它們。因此,我們最終會對於常規協程擁有 __cocall__ 而沒有 __call__,而對於協程產生器則擁有 __call__ 而沒有 __cocall__
  6. 語法上要求括號也會引入許多新問題。

    以下程式碼:

    await fut
    await function_returning_future()
    await asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))
    

    看起來會像:

    cocall fut()  # or cocall costart(fut)
    cocall (function_returning_future())()
    cocall asyncio.gather(costart(coro1, arg1, arg2),
                          costart(coro2, arg1, arg2))
    
  7. PEP 3152 中沒有與 async forasync with 等效的功能。

協程產生器 (Coroutine-generators)

搭配 async for 關鍵字,理想上應具備「協程產生器」的概念——即包含 yieldyield from 表達式的協程。為了避免與常規產生器產生歧義,我們可能會要求在 yield 之前加上 async 關鍵字,而 async yield from 則會引發 StopAsyncIteration 異常。

雖然實作協程產生器是可能的,但我們認為它們超出了本提案的範疇。這是一個進階概念,應予以仔細考慮與平衡,且對目前產生器對象的實作會有重大變更。這應作為另一份獨立 PEP 的主題。

為何使用「async」與「await」關鍵字

async/await 在程式語言中並非新概念:

  • C# 很久以前就有這項功能 [5]
  • ECMAScript 7 加入 async/await 的提案 [2];另見 Traceur 專案 [9]
  • Facebook 的 Hack/HHVM [6]
  • Google 的 Dart 語言 [7]
  • Scala [8]
  • 將 async/await 加入 C++ 的提案 [10]
  • 以及許多其他較不流行的語言。

這是一個巨大的優勢,因為部分使用者已經具備 async/await 的經驗,且這使得在一個專案中處理多種語言變得更容易(例如 Python 搭配 ECMAScript 7)。

為何「__aiter__」不回傳一個 awaitable

PEP 492 在 CPython 3.5.0 中被接受時,將 __aiter__ 定義為一個方法,預期回傳一個可解析為非同步迭代器的可等待對象。

在 3.5.2 中(由於 PEP 492 是在臨時基礎上被接受的),__aiter__ 協定已更新為直接回傳非同步迭代器。

這項變更的動機是為了能在 Python 中實作非同步產生器。詳見 [19][20]

「async」關鍵字的重要性

雖然可以只實作 await 表達式並將所有至少包含一個 await 的函式視為協程,但這種方法會讓 API 設計、程式碼重構及其長期維護變得更加困難。

讓我們假設 Python 只有 await 關鍵字:

def useful():
    ...
    await log(...)
    ...

def important():
    await useful()

如果 useful() 函式被重構且有人移除了其中所有的 await 表達式,它就會變成一個常規 Python 函式,所有依賴它的程式碼(包括 important())都會出錯。為了緩解此問題,必須引入一個類似於 @asyncio.coroutine 的裝飾器。

為何使用「async def」

對於某些人來說,單純的 async name(): pass 語法可能比 async def name(): pass 更有吸引力。輸入起來確實比較容易。但另一方面,它破壞了 async defasync withasync for 之間的對稱性,其中 async 是一個修飾符,說明該陳述式是非同步的。這也與現有語法更加一致。

為何不使用「await for」與「await with」

「async」是一個形容詞,因此它是「陳述式限定 (statement qualifier)」關鍵字的更好選擇。await for/with 會暗示某物正在等待 forwith 陳述式的完成。

為何是「async def」而非「def async」

async 關鍵字是一個「陳述式限定符」。一個很好的類比是其他語言中的「static」、「public」、「unsafe」關鍵字。「async for」是一個非同步「for」陳述式,「async with」是一個非同步「with」陳述式,「async def」是一個非同步函式。

將「async」放在主陳述式關鍵字之後可能會引入一些混淆,例如「for async item in iterator」可以被解讀為「對於迭代器中的每個非同步項目」。

async 關鍵字放在 defwithfor 之前也讓語言語法更簡單。而且「async def」在視覺上能更好地將協程與一般函式區分開來。

為何不使用 __future__ 導入

過渡計畫 章節說明了如何修改標記解析器 (tokenizer),使其「僅」在 async def 區塊中將 asyncawait 視為關鍵字。因此,async def 填補了原本需要模組級編譯器宣告(如 from __future__ import async_await)才能完成的角色。

為何魔術方法以「a」開頭

新的非同步魔術方法 __aiter____anext____aenter____aexit__ 都以相同的前綴「a」開頭。另一種建議是使用「async」前綴,使 __anext__ 變為 __async_next__。然而,為了使新魔術方法與現有的方法(如 __radd____iadd__)保持一致,決定使用較短的版本。

為何不重複使用現有的魔術名稱

關於新的非同步迭代器與上下文管理器的另一個想法是重複使用現有的魔術方法,透過在其宣告中加入 async 關鍵字:

class CM:
    async def __enter__(self): # instead of __aenter__
        ...

這種方法有以下缺點:

  • 將無法建立一個同時能在 withasync with 陳述式中運作的對象;
  • 它會破壞回溯相容性,因為在 Python <= 3.4 中,沒有任何規定禁止從 __enter__ 和/或 __exit__ 回傳類 Future 對象;
  • 本提案的主要重點之一是讓原生協程盡可能簡單且不容易出錯,因此協定必須明確區分。

為何不重複使用現有的「for」與「with」陳述式

現有的基於產生器的協程與本提案背後的願景是讓使用者能輕易看見程式碼可能暫停的地方。讓現有的「for」與「with」陳述式識別非同步迭代器與上下文管理器,將不可避免地建立隱含的暫停點,使程式碼的推論變得更加困難。

推導式 (Comprehensions)

可以提供非同步推導式的語法,但此建構超出了本 PEP 的範疇。

非同步 lambda 函式

可以提供非同步 lambda 函式的語法,但此建構超出了本 PEP 的範疇。

效能

整體影響

本提案不會引入可察覺的效能影響。以下是 Python 官方基準測試集的輸出結果 [4]

python perf.py -r -b default ../cpython/python.exe ../cpython-aw/python.exe

[skipped]

Report on Darwin ysmac 14.3.0 Darwin Kernel Version 14.3.0:
Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64
x86_64 i386

Total CPU cores: 8

### etree_iterparse ###
Min: 0.365359 -> 0.349168: 1.05x faster
Avg: 0.396924 -> 0.379735: 1.05x faster
Significant (t=9.71)
Stddev: 0.01225 -> 0.01277: 1.0423x larger

The following not significant results are hidden, use -v to show them:
django_v2, 2to3, etree_generate, etree_parse, etree_process, fastpickle,
fastunpickle, json_dump_v2, json_load, nbody, regex_v8, tornado_http.

標記解析器 (Tokenizer) 修改

使用修改過的標記解析器解析 Python 檔案沒有可察覺的減速:解析一個 12Mb 的檔案(Lib/test/test_binop.py 重複 1000 次)花費的時間相同。

async/await

以下微基準測試用於確定「async」函式與產生器之間的效能差異:

import sys
import time

def binary(n):
    if n <= 0:
        return 1
    l = yield from binary(n - 1)
    r = yield from binary(n - 1)
    return l + 1 + r

async def abinary(n):
    if n <= 0:
        return 1
    l = await abinary(n - 1)
    r = await abinary(n - 1)
    return l + 1 + r

def timeit(func, depth, repeat):
    t0 = time.time()
    for _ in range(repeat):
        o = func(depth)
        try:
            while True:
                o.send(None)
        except StopIteration:
            pass
    t1 = time.time()
    print('{}({}) * {}: total {:.3f}s'.format(
        func.__name__, depth, repeat, t1-t0))

結果是沒有可察覺的效能差異:

binary(19) * 30: total 53.321s
abinary(19) * 30: total 55.073s

binary(19) * 30: total 53.361s
abinary(19) * 30: total 51.360s

binary(19) * 30: total 49.438s
abinary(19) * 30: total 51.047s

請注意,深度 19 意味著 1,048,575 次呼叫。

參考實作

參考實作可見於:[3]

高階變更與新協定列表

  1. 用於定義協程的新語法:async def 與新的 await 關鍵字。
  2. 用於類 Future 對象的新 __await__ 方法,以及 PyTypeObject 中新的 tp_as_async.am_await 插槽。
  3. 用於非同步上下文管理器的新語法:async with。以及包含 __aenter____aexit__ 方法的相關協定。
  4. 用於非同步迭代的新語法:async for。以及包含 __aiter____anext__ 與新內建異常 StopAsyncIteration 的相關協定。PyTypeObject 中新的 tp_as_async.am_aitertp_as_async.am_anext 插槽。
  5. 新的 AST 節點:AsyncFunctionDef, AsyncFor, AsyncWith, Await
  6. 新函式:sys.set_coroutine_wrapper(callback), sys.get_coroutine_wrapper(), types.coroutine(gen), inspect.iscoroutinefunction(func), inspect.iscoroutine(obj), inspect.isawaitable(obj), inspect.getcoroutinestate(coro), 以及 inspect.getcoroutinelocals(coro)
  7. 程式碼對象的新位元旗標:CO_COROUTINECO_ITERABLE_COROUTINE
  8. 新的 ABC:collections.abc.Awaitable, collections.abc.Coroutine, collections.abc.AsyncIterable, 以及 collections.abc.AsyncIterator
  9. C API 變更:新的 PyCoro_Type(在 Python 中暴露為 types.CoroutineType)與 PyCoroObjectPyCoro_CheckExact(*o) 用於測試 o 是否為「原生協程」。

雖然變更與新內容的清單並不短,但重要的是要理解,大多數使用者不會直接使用這些功能。其設計目的是用於框架與函式庫,透過 async defawaitasync forasync with 語法為使用者提供方便使用且無歧義的 API。

運作範例

本 PEP 提出的所有概念皆已實作 [3] 並且可以進行測試。

import asyncio

async def echo_server():
    print('Serving on localhost:8000')
    await asyncio.start_server(handle_connection,
                               'localhost', 8000)

async def handle_connection(reader, writer):
    print('New connection...')

    while True:
        data = await reader.read(8192)

        if not data:
            break

        print('Sending {:.10}... back'.format(repr(data)))
        writer.write(data)

loop = asyncio.get_event_loop()
loop.run_until_complete(echo_server())
try:
    loop.run_forever()
finally:
    loop.close()

接受

Guido 已於 2015 年 5 月 5 日星期二接受 PEP 492 [14]

實作

實作進度追蹤於 issue 24017 [15]。已於 2015 年 5 月 11 日提交。

參考文獻

致謝

感謝 Guido van Rossum、Victor Stinner、Elvis Pranskevichus、Andrew Svetlov、Łukasz Langa、Greg Ewing、Stephen J. Turnbull、Jim J. Jewett、Brett Cannon、Alyssa Coghlan、Steven D’Aprano、Paul Moore、Nathaniel Smith、Ethan Furman、Stefan Behnel、Paul Sokolovsky、Victor Petrovykh 以及許多其他人對於本 PEP 的回饋、想法、編輯、批評、程式碼審查與討論。


來源:https://github.com/python/peps/blob/main/peps/pep-0492.rst

最後修改時間:2025-02-01 08:55:40 GMT