Asyncioは、大雑把に言えば、PythonでNodeのような非同期プログラムを行えるようにするモジュールです。Pythonでは、スレッド(concurrent.futures)を用いて並行プログラムを書くことができますが、Asyncioではもう少し軽量の並行プログラムを実現できます。
この辺は「Fluent Python」に詳しいですが、いかんせんAsyncioの箇所は最新のPythonのバージョンで大きく変更されています。以下の公式サイトが貴重な情報源になります。
ここでは、最も有名なAsyncioライブラリであるaiohttpを使ったソースコードを説明することで、Asyncioの概念を見ていきたいと思います。
以下の関連記事を投稿しました
(追加 2021/02/13) Django 3.1のAsync Views - Qiita
(追加 2022/09/05) Python Asyncio で作る Socket Server
1. ソースコード
以下のプログラムは、3つのサイト(どれもアマゾンのランキングページです)を並行的にスクレイピングするものです。このプログラムの注意点は、BeautifulSoupを使ってhtmlを解析しているので、アマゾンの方でページのHTMLを変えられたら、プログラムも変更が必要となります。
import asyncio
import aiohttp
import datetime
from bs4 import BeautifulSoup
async def main():
async with aiohttp.ClientSession() as session:
sites = [("https://www.amazon.co.jp/gp/top-sellers/books/ref=crw_ratp_ts_books", '本の売れ筋ランキング'),
("https://www.amazon.co.jp/gp/bestsellers/books/466298/ref=zg_bs_nav_b_1_b", 'コンピュータ・ITの売れ筋ランキング' ),
("https://www.amazon.co.jp/gp/bestsellers/books/492352/ref=zg_bs_nav_b_2_466298", 'プログラミングの売れ筋ランキング')]
tasks = [asyncio.create_task(one_site(session, *site)) for site in sites]
# tasks = [one_site(session, *site) for site in sites]
outs = await asyncio.gather(*tasks)
for out in outs:
print(out)
async def one_site(session, url, title):
today = datetime.date.today().strftime('%Y/%m/%d')
out = "-"*15 + f'{title} ({today})' + "-"*15 + "\n"
async with session.get(url) as resp:
data = await resp.text()
soup = BeautifulSoup(data, "lxml")
for i, el in enumerate(soup.find_all("li", class_="zg-item-immersion")):
name = el.find_all("div", class_="p13n-sc-truncate")[0].string.strip()
price = el.find("span", class_="p13n-sc-price") # .string.strip()
price = price.string.strip() if price is not None else "???"
out = out + f"[{i+1}] {name} ({price})\n"
return(out)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
以下が実行例となります。
---------------本の売れ筋ランキング (2021/02/01)---------------
[1] ももクロゲッタマン体操 パワー炸裂! 体幹ダイエット DVD67分付き (¥1,760)
[2] 呪術廻戦 1-13巻 新品セット (¥6,292)
[3] くびれ母ちゃんの骨からボディメイク - 3DX BODY - (美人開花シリーズ) (¥1,650)
[4] 【Amazon.co.jp 限定】TVガイドVOICE STARS vol.17 Amazon限定表紙版 (¥1,430)
[5] スマホ脳 (新潮新書) (¥1,078)
[6] デザインのひきだし42 (¥4,200)
[7] オードリー・タン デジタルとAIの未来を語る (¥1,980)
[8] 推し、燃ゆ (¥1,540)
[9] 1日1話、読めば心が熱くなる365人の仕事の教科書 (¥2,585)
[10] 【Amazon.co.jp 限定】味わいリッチな焼き菓子レシピ(特典:米粉のクッキー、アップルパイマフィンレシピ データ配信) (¥1,540)
---
[50] ダービースタリオン 公式全書 (¥2,420)
---------------コンピュータ・ITの売れ筋ランキング (2021/02/01)---------------
[1] iPadはかどる! 仕事技2021(全iPad・iPadOS 14対応/リモートワークにも最適な仕事技が満載) (¥1,320)
[2] スマホ脳 (新潮新書) (¥1,078)
[3] オードリー・タン デジタルとAIの未来を語る (¥1,980)
[4] できる イラストで学ぶ 入社1年目からのExcel VBA できる イラストで学ぶシリーズ (¥1,762)
[5] いちばんやさしいアジャイル開発の教本 人気講師が教えるDXを支える開発手法 「いちばんやさしい教本」シリーズ (¥1,584)
[6] プログラミング超初心者が初心者になるためのPython入門(1) セットアップ・文字列・数値編 (¥250)
[7] 一生使えるプレゼン上手の資料作成入門 一生使えるシリーズ (¥1,584)
[8] 2040年の未来予測 (¥1,870)
[9] iPad仕事術!SPECIAL 2020(手書きノート大特集! !) (¥990)
[10] 起業の天才!: 江副浩正 8兆円企業リクルートをつくった男 (¥2,200)
---
[50] ヒョーゴノスケ流 イラストの描き方 (¥2,178)
---------------プログラミングの売れ筋ランキング (2021/02/01)---------------
[1] プログラミング超初心者が初心者になるためのPython入門(1) セットアップ・文字列・数値編 (¥250)
[2] 【Amazon.co.jp 限定】 1冊ですべて身につくHTML & CSSとWebデザイン入門講座 (DL特典: CSS Flexbox チートシート) (¥2,486)
[3] まいぜんシスターズとマイクラを遊ぼう! (扶桑社ムック) (¥1,100)
[4] C#を勉強する順番: 文法プログラマーを卒業する方法 (¥99)
[5] リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice) (¥2,640)
[6] 【Swift】作って学ぼうiOSアプリ開発 (¥1,035)
[7] ゼロからFlaskがよくわかる本: Pythonで作るWebアプリケーション開発入門 (¥630)
[8] スッキリわかるJava入門 第3版 (スッキリシリーズ) (¥2,860)
[9] 実践Data Scienceシリーズ データ分析のためのデータ可視化入門 (KS情報科学専門書) (¥3,520)
[10] 独学プログラマー Python言語の基本から仕事のやり方まで (¥2,420)
---
[50] 基礎から学ぶ Ruby on Rails: 1週間の短期間講座!楽しく学ぶRailsの新しい入門書 (¥650)
2. 解説
以下、この例をasyncioモジュールの側面から見ていきます。
2-1.コルーチン
async defで宣言された関数のようなものはコルーチン関数と呼ばれ、callされるとコルーチン・オブジェクトを返します。以下コルーチン・オブジェクトを単に コルーチン と呼びます。
>>> async def f():
... return 1
...
>>> coro = f()
>>> type(coro)
<class 'coroutine'>
>>> coro.send(None) # コルーチン開始
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 1 # return 1
>>>
通常のやり方ではないですが手動でコルーチンを動かしてみます。メソッド send(None) でコルーチンを開始してみましょう。コルーチンは実行が終了したらStopIteration例外を上げます。コルーチン関数が return 1 しているので、StopIteration例外の値は 1 となります。
通常のプログラムにおいては、コルーチンは以下のようにawaitで実行され、その戻り値としてコルーチンの実行結果を受け取ることができます。但し、大元のmain()のコルーチンは asyncio.run() で実行します。
>>> import asyncio
>>> async def main():
... result = await f()
... print(result)
...
>>> asyncio.run(main())
1
aiohttp_client.pyにおいては、aiohttpモジュールの公式サイトにならって、asyncio.run()は使っていません。loop.run_until_complete() で実行しています。asyncio.run()では内部的にいろいろなことを行っており、aiohttpモジュールと相性が悪いようです。ちなみにloop.run_until_complete()もコルーチンを実行する関数です。asyncio.run()もloop.run_until_complete()を内部で利用していますが、それ以外に未完のタスクのキャンセルやloop.close()処理など多くの仕事をしています。
2-2.イベントループ
イベントループは、コルーチンの実行をスケジュールし、コルーチン同士の実行を切り替えたり、StopIteration例外を処理したりします。それ以上にソケットやファイルのイベントを処理したりもします。
イベントループはasyncioによる非同期システムの心臓ですが、裏方ですのであまり表には出ません。例えばaiohttp_client.pyでは以下のように使われています。
loop = asyncio.get_event_loop() # イベントループの取得
loop.run_until_complete(main()) # コルーチンmain()をイベントループに登録し終了するまで走らせる。
2-3.タスク
タスクはコルーチンのラッパーで、コルーチンをイベントループに登録しスケジュールしたものです。asyncio.create_task(コルーチン) で作成します。
以下の公式サイトの例で説明します。
asyncio.run()でイベントループを初期化・開始させ、 asyncio.create_task()で2つのタスクをイベントループ状に登録し、並行実行しているものです。
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay) # I/Oバウンド、終了するまで制御をイベントループに返します
print(what)
async def main():
task1 = asyncio.create_task( # taskの作成と実行
say_after(1, 'hello'))
task2 = asyncio.create_task( # taskの作成と実行
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1 # task1の実行終了を待つ
await task2 # task2の実行終了を待つ
print(f"finished at {time.strftime('%X')}")
asyncio.run(main()) # イベントループの作成と実行。コルーチンmain()を走らせる。
以下が実行結果です。同期的な処理なら3秒かかるところ、非同期の並行処理なので2秒しかかかっていないのがわかります。asyncio.sleep(delay)が並行して実行されているからです。
python asyncio_concurrent.py
started at 09:34:56
hello
world
finished at 09:34:58
また asyncio.gather(*tasks) はタスクリストtasksを同時並行処理し、awaitableを返します。
2-4.asyncio版コンテキストマネージャ
async with でasyncio版コンテキストマネージャを使うことができます。通常のコンテキストマネージャとと違うのは def enter と def exit メソッドの代わりに、async def aenter と async def aexit メソッドを定義することです。またcontextlibモジュールとasynccontextmanagerデコレーターを使ってより簡単に定義できるところも通常のコンテキストマネージャと同様です。
aiohttp_client.pyで言うと以下の2行で asyncio版コンテキストマネージャが使われています。
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
以上で終わります。