跳到主要內容

[Python] async def & await 重點整理

最近實習要用到 FastAPI,我發現 FastAPI 的 path operation function 會使用 async def,還會搭配使用 await,因為對這兩個關鍵字沒很熟,所以就藉機紀錄一下,也避免之後忘記。

async def & await 使用情境

我直接利用下面這個例子來展示什麼情況下可以使用 asyncawait

import time def dosomething(i): print(f"第 {i} 次開始") time.sleep(2) print(f"第 {i} 次結束") if __name__ == "__main__": start = time.time() for i in range(5): dosomething(i+1) print(f"time: {time.time() - start} (s)")

執行後應該會像這樣。

第 1 次開始
第 1 次結束
第 2 次開始
第 2 次結束
第 3 次開始
第 3 次結束
第 4 次開始
第 4 次結束
第 5 次開始
第 5 次結束
time: 10.048049688339233 (s)

這非常直覺,因為每次呼叫 dosomething() 時都會等待2秒,等完才會執行下一輪,所以最後執行總時間是10秒相當合理。

但仔細想想,如果那2秒是做網路請求或檔案讀寫(IO),這2秒是不需要CPU的,但CPU就只能發呆2秒,痴痴地等待回傳結果,其他什麼事都不能做,豈不是太浪費了嗎!? (學過作業系統的人就知道,絕對不能讓CPU發呆XD)

因此 Python 就有了 asyncio 這個工具,來徹底的利用(X) 榨乾(O) CPU的效能。

我把剛才的例子改成 asyncio 的版本。

import time import asyncio async def dosomething(i): print(f"第 {i} 次開始") await asyncio.sleep(2) print(f"第 {i} 次結束") if __name__ == "__main__": start = time.time() tasks = [dosomething(i+1) for i in range(5)] asyncio.run(asyncio.wait(tasks)) print(f"time: {time.time() - start} (s)")

執行結果會變成這樣,只需要2秒就結束了!

第 2 次開始
第 1 次開始
第 3 次開始
第 4 次開始
第 5 次開始
第 2 次結束
第 3 次結束
第 5 次結束
第 1 次結束
第 4 次結束
time: 2.011152982711792 (s)

為什麼會這樣呢? 其實 await 就是告訴 CPU 說後面這個函數很慢,不需要等它執行完畢。因此此時 CPU 就可以先跳去執行其他的事情,只需要在這個函數結束時再回來處理就好。這就是為什麼速度會快很多。

了解 async def & await 使用情境之後,就來說明一些細節吧!

coroutine

首先,async def & await 是 Python 3.5+ 之後才出現的 語法糖,目的是讓 coroutine 之間的調度更加清楚。

那就要先了解什麼是 coroutine

根據 Python 官方對 coroutine 定義:

Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points. They can be implemented with the async def statement.

簡單講 coroutine 可以在任意的時間點開始、暫停和離開,並且透過 async def 宣告此函數為一個 coroutine。

所以 await 的作用就是告訴 CPU 說可以暫停後面的工作,先去執行其他程式。另外 await 只能在 coroutine 中宣告,這就是為什麼 await 必須寫在 async def 裡面。

另一個要注意的點,await 後只能接 awaitables 物件,awaitables 物件就包括 coroutine, Task, Future 和有實作 __await__() 的物件。所以並不是所有函數都可以使用 await 加速。

coroutine 實作

最後來講如何實作 coroutine 吧!

import asyncio async def main(): await asyncio.sleep(1) print('hello') main()

執行後應該會出錯:

RuntimeWarning: coroutine 'main' was never awaited
  main()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

這是因為現在 main() 已經宣告成一個 coroutine 了,所以不能夠直接呼叫,而是要改成用 asyncio.run() 呼叫,所以將程式碼改成下面這樣就可以成功印出 hello 了。

import asyncio async def main(): await asyncio.sleep(1) print('hello') asyncio.run(main())

好,async def & await 就大致介紹到這邊,關於 asyncio 有滿多東西可以玩的,有需要歡迎看這篇 Python asyncio 從不會到上路

FastAPI async def & await

最後回來看 FastAPI 文件 中怎麼說明 async def & await 的。

他說如果你需要在 path operation function 中呼叫一些很慢的函數 (如: 讀取資料庫、網路請求…) 時,就可以使用 async defawait 來加速,就像下面的例子。

@app.get('/') async def read_results(): results = await do_something() # do_something() is slow return results

但如果不需要呼叫這些函數,就直接使用一般 def 即可。

現在就能明白為何 FastAPI 要使用 async defawait 了吧!

參考資料


如果喜歡這篇文章,請訂閱我並且拍五下手給予回饋(使用Google或Facebook帳號免費登入,只需要30秒),資金由LikeCoin提供,完全不會花到各位半毛錢!

因為您的支持,我才更有動力創作出更優質的文章~

留言

這個網誌中的熱門文章

[Python] 關於 with 你所不知道的事

相信用 C 語言寫過檔案讀取的人都知道,經常開檔後就忘記關檔,或者程式中間跳出例外,因此沒有關檔。這些問題常常讓人非常頭痛😱。 所幸 Python 中有 with 這個語法糖,可以自動幫你開關檔,跳出例外也難不倒他,真的非常好用。 但你真的了解 with 背後的運行原理嗎? 🤔 不知道沒關係,因為你現在就會知道了! context manager 在講 with 之前,必須先介紹一下 context manager ,中文可以翻成 情境管理器 。 為什麼叫做 情境管理器 呢? 我們試想一個情境… 當你進入房間時,就要開啟房間的燈:然後當你離開房間時,就要關閉房間的燈。 像這樣理所當然一定要做的事情,我們就稱為一個情境。而 Python 就是透過情境管理器處理這些情境。 要自己實作 context manager 其實很簡單,只要在 Class 中實作 __enter__() 和 __exit__() 即可。 就以進入房間當作例子。 class Room (): def turn_on_light ( self ): print ( "開燈" ) def turn_off_light ( self ): print ( "關燈" ) def __enter__ ( self ): print ( "進入房間" ) self.turn_on_light() return "在房間裡" def __exit__ ( self, exc_type, exc_value, traceback ): self.turn_off_light() print ( "離開房間" ) with Room() as room: ...

[遊記] 2022/07/22 南寮漁港、香山濕地

前言: 2022年的暑假,我來到新竹的工研院實習,因此有了兩個月好好探索這座陌生城市的機會。我在來之前就計畫好了,每周五要選一個地方去旅行,目標是在兩個月內把整個新竹玩透透! 來到了第三個禮拜,今天我約了新竹在地人的大學朋友,請他騎機車載我到處逛逛😆 不過因為他早上有事情,所以我們就約中午吃飯。中午我們去吃城隍廟附近的 阿桂羊牛雜 ,我點了朋友很推的 羊肉炒麵 ,這家的炒麵很特別,醬汁很濃稠,沙茶味很香~ 我點加辣但有點太辣了,下次可能點小辣就好。另外有附飲料和冷氣這點很加分。 (只顧跟朋友聊天,就忘記拍照了😂) 阿桂羊牛雜 羊肉炒麵 $100 推薦指數:4⭐ 吃完飯後,本來想去南寮漁港,但因為今天的太陽真的太大了! 所以朋友就提議先去 新竹巨城 吹冷氣,晚點再去南寮漁港。雖然已經來過巨城一次,但跟別人一起逛就是不太一樣。我們去逛了服飾店、書局和湯姆熊,不知不覺就三點了。於是就離開巨城前往南寮漁港囉~ 本來以為 南寮漁港 很遠,但騎機車一下子就到了,果然在新竹還是要有機車比較方便阿! 我們先去南寮漁港的遊客中心,展望台的景色很不錯,室內還有溜滑梯可以玩呢! 接著我們在附近的魚市場、國際風箏場等地方邊聊邊走,最後走到 魚鱗天梯 。 魚鱗天梯看起來的確很像魚鱗,但聽說他的功能其實是消波塊,還真酷! 底下就有一小片沙灘,因為我今天穿拖鞋,就有下去踩一下海水。不過這裡的海水沒很乾淨,上來後腳上全都是沙子,幸好旁邊就有可以洗腳的地方。 其實旁邊有個滿有名的 17公里海岸自行車道 ,不過我們比較晚才到,所以就沒租腳踏車去騎了。 接著就往南到 香山濕地 ,騎機車也是一下就到了。 香山濕地就像小型的高美濕地,一旁的 賞蟹步道 可以直接走在溼地上方。 賞蟹步道兩旁真的很多螃蟹,照片裡的白點都是螃蟹哦! 當時剛好碰上漲潮,於是我們就在步道上拍起縮時攝影,從影片中可見漲潮的速度有多快! 香山濕地也是看夕陽的好景點,只是有點太早來了,於是我們走去旁邊的 綠色隧道 ,等待夕陽下山。 最後終於等到夕陽了! 加上倒影還滿漂亮的,只可惜今天海面有點雲,無緣看到夕陽落到海平面之下的景色。 最後順路繞去附近的 青青草原 ,雖然天色已經暗了下來,不過因此溜滑梯都不用排隊,可以多溜了幾趟😁 我們也去看了一眼大草原,但因為傍晚有一堆蚊蟲,所以就趕緊撤退了! 最後晚餐去吃 蛋包飯 ,這家也是朋友推薦的...

[2021 IT鐵人賽] Day 23:專案05 - KKBOX風雲榜02 | AJAX

昨天已經找到的KKBOX用來傳資料的API,也知道各個參數的意義了,今天就實際將資料抓下來吧! 歌曲資訊 回到昨天那個API,是用JSON格式傳遞資料,資料的格式大致如下: 我們可以發現新歌的資料都放在 “newrelease” 之下,一個element就是一首歌的資訊,另外,每首歌的資訊也以key:value的形式整理的很清楚。 接著,就用之前教過的 requests.get(url) 直接取得API回傳的資料,但回傳的型態是json字串,所以再用Python本身內建的 json.loads() 函數轉成Python的list和dict資料型態。 # KKBOX華語新歌日榜 url = "https://kma.kkbox.com/charts/api/v1/daily?category=297&lang=tc&limit=50&terr=tw&type=newrelease" # 取得歌曲資訊json檔 response = requests.get(url) # 將json字串轉為Python的字典型態 data = json.loads(response.text) 既然已經轉成list和dict的型態了,再根據剛才觀察API得知的架構,要篩選資料就非常簡單,直接來看程式碼: song_list = data[ "data" ][ "charts" ][ "newrelease" ] # 取得每首歌的排名、曲名、連結、作者、時間 for song in song_list: song_rank = song[ "rankings" ][ "this_period" ] song_name = song[ "song_name" ] song_url = song[ "song_url" ] song_artist = song[ "artist_name"...