8.10. 可等待類別

8.10.1. 什麼是可等待物件

當協程寫下 await x 時,語言會詢問 x 該如何等待它。任何知道如何回答的物件都是可等待的(awaitable)。共有兩種:

  • async def 函式回傳的協程物件。每次呼叫 send_request() 都會產生一個這樣的物件 -- 是協程物件,而非函式的結果。寫下 await send_request() 才是真正執行其主體的動作。

  • 定義了 __await__ 方法的類別所產生的物件。async def 是第一種的簡寫;__await__ 則是手動製作第二種的方式。

應用程式碼預設使用第一種。第二種存在是為了少見的情況:當物件本身需要成為呼叫端 await 的對象,而非呼叫該物件某個方法所得到的結果時。

8.10.2. 兩種形式並列對照

同樣的邏輯分別以協程與可等待類別寫出:

import asyncio

# async def -- almost always the right choice
async def yield_then_return(value):
    await asyncio.sleep_ms(0)
    return value

# awaitable class -- equivalent, rarely written
class YieldThenReturn:
    def __init__(self, value):
        self._value = value

    def __await__(self):
        yield                       # let the loop run once
        return self._value          # value of the ``await`` expression

兩者都可以用相同的方式被 await:

async def main():
    a = await yield_then_return(1)
    b = await YieldThenReturn(2)
    print(a, b)                     # prints: 1 2

async def 形式較短、讀起來像一般的 Python,並讓語言管理所有暫停與恢復的記帳工作。在這個範例中,類別形式並未得到任何額外好處。

8.10.3. 類別形式何時值得使用

當物件必須成為應用程式傳遞的某個東西 -- 而非呼叫端為了取得協程而呼叫的函式 -- 時,類別形式是唯一的選擇:

  • 一個類 future(future-like)的值,由應用程式建立、交給生產者,稍後再 await 以取得所產生的結果。

  • 建立在現有原語之上的自訂原語,其自然的 API 是 await my_thing 而非 await my_thing.wait()asyncio.Task 類別本身就是一個內建範例 -- 之所以可以寫 await task,是因為 Task 定義了 __await__

對於任何符合呼叫一個函式、取得一個協程、await 它這種形式的情況,async def 都勝出。

8.10.4. 協定細節

__await__ 是一個普通方法(不是 async def),它會回傳一個迭代器。將它寫成產生器 -- 在主體中至少包含一個 yield -- 就會自動使它成為迭代器。每個 yield 都會暫停 await 該物件的協程,並將控制權交還給事件迴圈;產生器會在迴圈下次排程該任務時恢復。單純的 return(或執行到結尾)會結束等待;return 所產生的值會成為 await 運算式的值。

實務上,少數會這麼做的類別會把 yield 的工作交給現有的可等待物件,而非自行驅動迴圈:

class WaitForEvent:
    def __init__(self, event):
        self._event = event

    def __await__(self):
        yield from self._event.wait().__await__()
        return self._event

yield from 會重複使用底層事件的 __await__ 來進行實際的暫停。大多數 asyncio 應用程式永遠不需要寫出這兩種形式中的任何一種。