8.6. 例外

asyncio 指令碼中的例外行為幾乎與一般 Python 相同 -- 它們會沿著呼叫鏈向上傳播,直到某處將其捕捉。之所以說幾乎,是因為任務是並行執行的,所以「向上」的路徑並非當初建立任務的那條路徑。本頁將說明在每種常見形式中例外會傳到何處。

8.6.1. 在單一協程內

協程內的 try/except 會以一般方式捕捉它所 await 的任何項目所引發的例外:

async def fetch_with_retry(url):
    for attempt in range(3):
        try:
            return await fetch(url)
        except OSError as e:
            last_error = e
    raise last_error

這裡沒有任何 asyncio 特有之處 -- except 子句看到 fetch 內部引發的例外,就如同它是一次普通的函式呼叫一樣。

8.6.2. 在應用程式正在等待的任務中

當一個以 Task 身分執行的協程引發例外時,該例外會儲存在任務上。下次有某處 await 該任務時,例外就會在該 await 處被重新引發:

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

同樣的情況也適用於 asyncio.gather()。其預設行為 -- 一個子項引發例外,其他子項被取消,例外從 gather 中傳播出來 -- 正是源自這個機制。

8.6.3. 在無人等待的任務中

一個從來沒有人 await 的任務是需要特別注意的情況。例外仍然會發生;迴圈會注意到該任務以未處理的例外結束;但卻沒有 await 可供它浮現出來。預設行為是透過 sys.stderr 印出一份回溯訊息並繼續執行 -- 這對於無人監看的診斷工作來說沒問題,但對於想要知道情況的應用程式來說則不太合適。

正確的修正方式通常是等待該任務。可以直接這麼做,記住其控制代碼並在關閉期間等待它;或者透過 gather()wait_for() 隱含地等待。逾時與取消 頁面的「關閉應用程式」模式正是為典型指令碼所衍生的那些長期存活背景任務捕捉這種情況。

8.6.4. 自訂例外處理器

當靜默地印出回溯並繼續執行還不夠時,迴圈會公開一個掛鉤 -- Loop.set_exception_handler -- 應用程式可以覆寫它以做其他事情:

def handler(loop, context):
    print("asyncio:", context.get("message"))
    if "exception" in context:
        sys.print_exception(context["exception"])

loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)

context 引數是一個字典,鍵包含 'message''exception''future'。在某些警告式事件中可能會缺少例外,這就是範例使用 .get() 的原因。

典型用途包括將失敗記錄到快閃記憶體、閃爍一個錯誤 LED,或升級為看門狗重新開機。迴圈控制 頁面涵蓋了迴圈掛鉤的完整面向。

8.6.5. KeyboardInterrupt

當指令碼從外部被停止時 -- 通常是 IDE 要求它停止 -- 該請求會以 KeyboardInterrupt 的形式抵達指令碼內部。在 asyncio.run() 中,它會以任何其他未處理例外的方式傳播:main 被取消,迴圈正在追蹤的每個任務也都被取消,而 KeyboardInterrupt 則從 asyncio.run() 中被重新引發。finally 子句會在退出途中執行,因此取消頁面中的同一套清理模式正是處理它的方式。