コルーチンの定義と使い方

PythonでWeb APIから大量のデータを取得したり、多数のファイルを同時に読み書きしたりする際、1つの処理が終わるまで次の処理が完全にストップしてしまう状態を「ブロッキング(同期処理)」と呼びます。
この無駄な待ち時間をなくし、通信の返事を待っている間に別の処理をどんどん進める仕組みが「非同期処理(Asynchronous Processing)」です。そして、Pythonの非同期処理において、途中で処理を一時中断・再開できる特別な関数のことをコルーチン(Coroutine)と呼びます。本記事では、asyncio モジュールを用いたコルーチンの基礎から実践的な並行処理までを徹底解説します。
コルーチンの定義(asyncキーワード)
通常の関数は、呼び出されると最後の行(または return)まで一気に実行されます。一方、コルーチンは関数の定義の先頭に async def と記述します。
コルーチンの中で「時間がかかる処理(通信や待機)」を行う場所には、await キーワードを配置します。await があると、プログラムはその処理の完了をじっと待つのではなく、呼び出し元のイベントループ(処理を管理する司令塔)に一時的に実行権を返却し、別の処理を優先して進めることができます。
import asyncio
# async def でコルーチン関数を定義する
async def my_coroutine():
print("コルーチンが始まりました")
# 💡 time.sleep() を使うとプログラム全体がフリーズ(ブロック)するため、
# 非同期処理では必ず asyncio.sleep() を使い、頭に await を付けます。
await asyncio.sleep(1) # 1秒間、実行権を周囲に譲って非同期待機
print("1秒待機後、コルーチンが終了しました")
# 【重要】コルーチンは my_coroutine() と呼び出すだけでは実行されません。
# 司令塔(イベントループ)を起動する asyncio.run() に渡すことで初めて動作します。
asyncio.run(my_coroutine())
複数のコルーチンを同時に走らせる(asyncio.gather)
コルーチンの真価は、複数の重い処理を「同時に並行して実行」したときに発揮されます。これを行うには、複数のコルーチンをまとめてイベントループに投入する asyncio.gather() を使用します。
import asyncio
import time
async def fetch_data_1():
print("データ1の取得開始...")
await asyncio.sleep(2) # 2秒かかる通信を想定
print("データ1の取得完了!")
async def fetch_data_2():
print("データ2の取得開始...")
await asyncio.sleep(1) # 1秒かかる通信を想定
print("データ2の取得完了!")
async def main():
start_time = time.time()
# 2つのコルーチンを同時にイベントループで並行実行
await asyncio.gather(fetch_data_1(), fetch_data_2())
end_time = time.time()
print(f"全体の処理時間: {end_time - start_time:.2f} 秒")
if __name__ == '__main__':
asyncio.run(main())
上記のコードを実行すると、まず「データ1」と「データ2」の取得がほぼ同時に開始されます。先に1秒待機のデータ2が終わり、その1秒後にデータ1が終わります。通常の同期処理であれば 2秒 + 1秒 = 3秒 かかるはずの処理が、非同期の並行処理によって約2秒(一番重い処理と同じ時間)で完了します。
コルーチンの実行結果(戻り値)の受け取り
コルーチンが return で値を返す場合、await を使ってその戻り値を直接変数に受け取ることができます。また、asyncio.gather() を使った場合は、各コルーチンの戻り値が投入した順番通りのリストとして返ってきます。
import asyncio
async def download_price(item_name):
await asyncio.sleep(1)
return f"{item_name}の価格: 500円"
async def main():
# パターンA: 1つずつ結果を待って受け取る
result_single = await download_price("リンゴ")
print(result_single)
# パターンB: 複数同時に走らせて、すべての結果をリストで回収する
# 完了した順ではなく、引数に並べた順(バナナ、ブドウ)で結果が格納されます。
results = await asyncio.gather(
download_price("バナナ"),
download_price("ブドウ")
)
print(results) # ['バナナの価格: 500円', 'ブドウの価格: 500円']
asyncio.run(main())
コルーチン内の例外処理(try-except)
非同期処理の実行中にネットワークエラーや計算エラーが発生した場合でも、通常のPythonコードと全く同じように try-except 構文で例外をキャッチすることができます。
import asyncio
async def web_request(url):
print(f"{url} へアクセス中...")
await asyncio.sleep(1)
if url == "https://error-url.com":
# 意図的に通信エラー(例外)を発生させる
raise ConnectionError("サーバーへの接続に失敗しました。")
return "正常なデータ"
async def main():
target_urls = ["https://example.com", "https://error-url.com"]
for url in target_urls:
try:
# awaitする箇所をtryで囲むことで、非同期内のエラーを安全にキャッチ可能
data = await web_request(url)
print(f"取得結果: {data}")
except ConnectionError as e:
print(f"【エラー検知】 {e}")
asyncio.run(main())
同期処理と非同期処理(コルーチン)の違いまとめ
プログラムの実行スタイルと挙動の違いの比較です。
| 比較項目 | 通常の関数(同期処理) | コルーチン(async/await 非同期処理) |
|---|---|---|
| 関数の定義方法 | def 関数名(): |
async def 関数名(): |
| 関数の呼び出し方 | 関数名() で即座に実行される。 |
await コルーチン() または asyncio.run() などのイベントループ経由が必須。 |
| 待ち時間(I/O)の挙動 | 処理が完了するまで、プログラム全体の進行が完全にストップ(ブロッキング)する。 | await の場所で他のコルーチンに処理を譲るため、時間を有効活用(ノンブロッキング)できる。 |
| 主なユースケース | 通常の計算、単純なスクリプト、順序が重要な処理。 | 大量のサードパーティAPI呼び出し、Webスクレイピング、非同期Webサーバー(FastAPIなど)の実装。 |
まとめ
asyncで宣言し、awaitで譲る:コルーチンは非同期用の特殊な関数であり、内部の待機処理でawaitを明記することで、他のタスクへ効率的に処理の実行権をバトンタッチできます。- 起動にはイベントループが必要:コルーチンは単体で呼び出しても中身は実行されません。必ず起点となる場所で
asyncio.run()などを実行し、非同期の世界を立ち上げる必要があります。 asyncio.gatherで並列化のメリットを最大化:複数のコルーチンを同時に並行実行させることで、ネットワークの応答待ちなどの無駄なブロッキング時間を極限まで削り、全体のパフォーマンスを劇的に向上させることができます。