跳到主要內容

[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提供,完全不會花到各位半毛錢!

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

留言

這個網誌中的熱門文章

[心得] COSCUP 2021 - 製播組志工經驗

前言 這次是 第二次參加COSCUP志工 ,第一次是2020年被朋友拉著報名場務組,當時對COSCUP完全不了解,連對Conference也是一點概念都沒有(畢竟高中從來沒有接觸過相關活動...),第一次就是懵懵懂懂的去參加了COSCUP。2020年雖然也有疫情,但情況並不像今年這麼嚴重,仍然是以實體方式舉辦,因為是第一次參加COSCUP,跟大家都不熟,所以基本上活動期間我就是黏著我的朋友,他做什麼我就跟著做什麼,不太敢跟其他人搭話。 即使如此,但因為COSCUP結束後和其他工作人員們一起去慶功宴,還是多認識了幾個人,之後仍然有在聯絡,後來之所以能參加SITCON和今年2021報名COSCUP志工,也都是因為他們的關係。因為有他們,我才會認識COSCUP,之後也才會參加SITCON,也才會受到很大的震撼,開始努力自學程式,這一切真的都要歸功於他們。 今年COSCUP在7/31~8/1舉辦,但是因為5月中疫情突然爆發,總召們討論過後最終決定以全程線上的方式舉辦,這對我來說影響非常大,因為我是報名場務組,因為全程線上就不需要實體場地了,場務組的人數也因此驟減,因此我面臨到了 失業 危機...。 最後的解決方式是: 協助我們轉職到其他組別 。這時因為線上的緣故,導播組(主要負責Youtube直播控場)的工作量大增,剛好我也對Youtube直播串流滿感興趣的,所以就轉職到製播組了!! 會前準備 在製播組的第一次會議中,組長請我們挑選自己想要的職位,我當初填導播(主要控制Youtube直播間的人),但最後因為網路太慢的問題,被分配為助理QQ,不過實際上做的事情是一樣的,權限也是一樣的,所以我也沒有特別在意職稱。 因為大概是前一個月左右才被調去製播組,滿臨時的,所以開會開得很緊湊,平均每一周開一次會,交代了如何使用StreamYard,以及跟主持人聯絡等注意事項,今年也許是因為都是全線上開會或者對COSCUP有新的觀點的關係,我在今年的參與度比去年高出許多,付出的時間也比去年來的多。而且我覺得最大的不同在於,我開始敢在會議中講話了,要是以前的我是完全不敢的。因為在會議中有必要開口表達意見的時候,慢慢就覺得沒這麼可怕了,這或許就是參加COSCUP志工帶給我的膽量! 會前兩周是最忙的時候,當時每天都在催影片,一周中有...

[2021 IT鐵人賽] Day 06:專案01 - 超簡單個人履歷05 | CSS版面佈局、Flex

昨天講完的CSS的文字和區塊屬性後,今天要接續介紹版面佈局的屬性,以及一個非常好用的佈局容器 - Flex,上完這堂課,你的網頁佈局就可以更加彈性囉~ 那麼,我們廢話不多說,就開始今天的介紹吧! CSS版面佈局 首先,你按 F12 打開開發人員工具,應該會在 Elements >> Styles 滑到最底下看到這個畫面(Chrome一定有,其他瀏覽器不確定): 記好這個圖,因為他就是CSS版面佈局的概念圖。 我們看這個圖,發現他像箭靶一樣一圈圈的包圍起來,主要有三層, 從外到內分別是margin、border和padding 。border我們昨天已經說過了,所以接下來我只著重在介紹margin和padding這兩個屬性該如何使用。 margin margin,又稱外距。顧名思義就是元素外側到其他元素或邊界的距離,通常用於在兩個元素間留下空間,畢竟東西都緊貼在一起也不好看對吧? 我們就用以下例子認識margin: HTML(都同一個,之後例子我就不放了): < div class = "outside" > < div class = "inside" ></ div > </ div > CSS: .outside { width : 200px ; height : 200px ; background-color : rgb ( 138 , 138 , 138 ); margin : 50px ; } .inside { width : 100px ; height : 100px ; background-color : rgb ( 92 , 92 , 92 ); } 顯示結果為: 我們按F12打開開發者工具,點上方的紅框的圖示,接著將游標移動到淺灰色的方塊上,就會顯示如同上方的畫面。 我們可以看到橘色的部分代表margin,往淺灰色的方塊上下左右推了50px的空間。 ...

[Python] 關鍵字yield和return究竟有什麼不同?

學習Scrapy的過程中碰到 yeild 這個關鍵字,我使用Python快半年了,還真的是第一次遇到這個關鍵字,於是我花了點時間研究後,終於明白它的作用了,怕下次看到時忘記,所以用這篇文將yield這個關鍵字重點整理一下。 1. yield的核心目的:為了節省記憶體 如果想要印出0~100的平方時,我們可能會這樣寫。 powers = [x**2 for x in range(100)] for x in powers: print(x) 但這樣有一個致命問題在於,必須把整個list都存放在記憶體中,100個元素可能還不成問題,但如果今天的對象是一百萬筆資料,記憶體可能會承受不了,程式就崩潰了。 接下來就會說明yield要如何節省記憶體,但在此之前,先來談談Python的生成器(generator)。 2. 什麼是生成器(generator)? 生成器是一個可迭代的物件,可以放在for迴圈的in前面,或者使用next()函數呼叫執行下一次迭代。 和列表的差別在於, 生成器會保存上次紀錄,並只有在呼叫下一層迭代的時候才載入記憶體執行 。 所以將上面的例子改寫成生成器,結果是一樣的,卻可以防止超過記憶體,注意我用的是 ( 而不是 [ 。 powers = (x**2 for x in range(100)) for x in powers: print(x) 3. 函數加入yield後不再是一般的函數,而被視作為生成器(generator) 呼叫函數後,回傳的並非數值,而是函數的生成器物件。 4. yield和return一樣會回傳值,不過yield會記住上次執行的位置 yield和return一樣都會回傳值並中斷在目前位置, 但最大不同在於yield在下次迭代時會從上次迭代的下一行接續執行 ,一直執行到下一個yield出現,如果沒有下一個yield則結束這個生成器。而且接續上一個迭代前的變數不會改變,就是維持上次結束前的模樣。 這部分我們來看下面這個例子: def yield_test(n): print("start n =", n) for i in range(n): yield i*i print("i =", i)...