圖片來自 Cameron Venti

如何在 Jupyter Notebook 跑 Python 異步程式

異步是 Python 3.4 起引入的重大變革,透過 asyncio 模組與新的 asyncawait 語法達到與 JavaScript 類似的異步執行效果。

一個最簡單的異步程式例子:

import asyncio

async def do_something():
    await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(do_something())

在上面的例子內有幾個 Python 異步程式的特徵:

  • async 關鍵字定義 do_something() 為異步函式。
  • 函式內有 await 關鍵字的敘述句表示該敘述可為異步執行。
  • await 後呼叫的函式 asyncio.sleep() 為一個具有 awaitable 的函式,對 await 關鍵字來說,後面呼叫的函式有 awaitable 屬性是必須的,否則會引發錯誤。
  • 取得一個 event loop 物件,並令此 event loop 物件去執行 do_something()

Event loop 物件可以接受一系列的 async 函式,並互相調度,在上面的例子中,因為只有單個任務,所以用異步顯得脫褲子放屁,在真實的場景中,event loop 接受多個 async 函式後,當遇到第一個函式的 await,異步機制就會開始作用,讓第一個函式的 await 繼續跑,並開始讓下一個函式也跑起來,以此類推直到所有 event loop 內的異步函式都跑完。

當在 Jupyter Notebook 裡跑異步程式

在 Jupyter Notebook 內可以直接使用 await 語法呼叫異步函式:

import asyncio

await asyncio.sleep(1)

就這麼簡單。

下面這些舊文可以視為廢文,或僅供參考。


Jupyter Notebook 是可互動的 Python 編寫環境,除了程式碼外,也可以塞入 Markdown 區塊,既可以跑程式又可以寫筆記,因此很常被拿來用於驗證 POC 的環境,但是把上面的程式拿去 Jupyter Notebook 環境下跑,卻會出現錯誤:

In[3] 區塊,我們呼叫了 run_until_complete(do_something()),卻跳出了「RuntimeError: This event loop is already running」的錯誤。

根據錯誤提示,我們往上追朔 In[2] 區塊的 loop,可以看到確實這個 loop 在取得的當下就已經是運行中的狀態了:

<_UnixSelectorEventLoop running=True closed=False debug=False>

之所以會這樣,是由於 Jupyter Notebook 這個環境也有使用異步的機制,因此我們用 get_event_loop() 會拿到的是 Jupyter Notebook 的那個 event loop 物件,因此在呼叫 run_until_complete() 時它才會提示「This event loop is already running」的錯誤,更深一層的原因是 Python 的 asyncio 的 event loop 原始設計是不允許巢狀結構的,也就是在 Jupyter 的 event loop 內無法再建立一層我們的 event loop。

了解完引發錯誤的原因,再回頭看上方 Jupyter Notebook 的 In[4] 區塊,在這邊我們改用 create_task() 把我們的異步函式 do_something() 加入成為現有 event loop 內的一個 Task,讓 event loop 去調度這個 Task 的運行。

Task 是 asyncio 的另一個概念,前面定義的異步函式都需要被顯式的或隱式的轉換成 Task 物件才能交給 event loop 去運行,Task 物件可以是一份任務,或者是 N 份任務的清單,而 Task 顧名思義,就是任務。

上面我們是透過調用 Jupyter 現有的 event loop 的 create_task(),讓我們能順利的在 Jupyter Notebook 內運行異步函式,但也有其它的思路可以達成,例如 nest-asyncio 套件把 asyncio 魔改成支援支援巢狀 event loop 的機制,讓我們在 Jupyter Notebook 的 event loop 內可以再創建另一層的 event loop,要用哪種方式還是取決於對 Python 異步機制的理解而定,如果讀者也像我一樣理解不了 asyncio 又想在 Jupyter 跑異步程式假會的話,可以參考上面的做法。