非同期 Python: 知っておくべきこと 🐍🐍🐍
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Python コルーチンの開発プロセスと新旧コルーチンの詳細な分析
1. Python コルーチンの歴史的進化
Python の長い開発の過程で、コルーチンの実装はいくつかの大きな変化を遂げてきました。これらの変化を理解することで、Python の非同期プログラミングの本質をより深く理解することができます。
1.1 初期探索と基本機能の導入
- Python 2.5: このバージョンでは、
.send()
、.throw()
、および.close()
メソッドがジェネレーターに導入されました。これらのメソッドの登場により、ジェネレーターは単なる単純なイテレーターではなくなりました。より複雑なインタラクション機能を持ち始め、コルーチンの開発のための一定の基盤を築きました。たとえば、.send()
メソッドはジェネレーターにデータを送信でき、ジェネレーターが単方向にしか出力できないという以前の制限を打ち破りました。 - Python 3.3:
yield from
構文が導入されました。これは重要なマイルストーンです。これにより、ジェネレーターは戻り値を受け取ることができ、yield from
を使用してコルーチンを直接定義できるようになりました。この機能により、コルーチンの記述が簡素化され、コードの可読性と保守性が向上しました。たとえば、yield from
を使用すると、複雑なジェネレーター操作を別のジェネレーターに簡単に委譲でき、コードの再利用と論理的な分離を実現できます。
1.2 標準ライブラリのサポートと構文の改良
- Python 3.4:
asyncio
モジュールが追加されました。これは、Python が最新の非同期プログラミングに向かうための重要なステップでした。asyncio
は、イベントループベースの非同期プログラミングフレームワークを提供し、開発者は非常に効率的な非同期コードをより便利に記述できます。イベントループやタスク管理などのコア機能を含む、コルーチンを実行するための強力なインフラストラクチャを提供します。 - Python 3.5:
async
およびawait
キーワードが追加され、構文レベルで非同期プログラミングに対するより直接的で明確なサポートが提供されました。async
キーワードは、関数がコルーチンであることを示す非同期関数を定義するために使用されます。await
キーワードは、コルーチンの実行を一時停止し、非同期操作の完了を待つために使用されます。この構文糖は、非同期コードの記述スタイルを同期コードの記述スタイルに近づけ、非同期プログラミングの敷居を大幅に下げます。
1.3 成熟と最適化の段階
- Python 3.7:
async def + await
を使用してコルーチンを定義する方法が正式に確立されました。この方法はより簡潔でわかりやすく、Python でコルーチンを定義する標準的な方法になりました。これにより、非同期プログラミングの構文構造がさらに強化され、開発者はより自然に非同期コードを記述できます。 - Python 3.10:
yield from
を使用したコルーチンの定義が削除され、Python コルーチンの開発における新しい段階が示されました。async
およびawait
に基づく新しいコルーチンシステムに重点が置かれ、さまざまな実装方法によって引き起こされる混乱が軽減されました。
1.4 新旧コルーチンの概念と影響
古いコルーチンは、yield
や yield from
などのジェネレーター構文に基づいて実装されています。一方、新しいコルーチンは、asyncio
、async
、await
などのキーワードに基づいています。コルーチンの開発の歴史の中で、2 つの実装方法には交差期間がありました。ただし、古いコルーチンのジェネレーターベースの構文により、ジェネレーターとコルーチンの概念が混同しやすく、学習者にいくつかの困難が生じました。したがって、コルーチンの 2 つの実装方法の違いを深く理解することは、Python の非同期プログラミングを習得するために非常に重要です。
2. 古いコルーチンのレビュー
2.1 コアメカニズム: yield
キーワードの魔法
古いコルーチンの中心は yield
キーワードにあり、コードの実行の一時停止と再開、関数間の交互実行、CPU リソースの転送など、強力な機能を関数に与えます。
2.2 コード例の分析
import time def consume(): r = '' while True: n = yield r print(f'[consumer] Starting to consume {n}...') time.sleep(1) r = f'{n} consumed' def produce(c): next(c) n = 0 while n < 5: n = n + 1 print(f'[producer] Produced {n}...') r = c.send(n) print(f'[producer] Consumer return: {r}') c.close() if __name__ == '__main__': c = consume() produce(c)
この例では、consume
関数はコンシューマーコルーチンであり、produce
関数はプロデューサー関数です。consume
関数の while True
ループにより、関数は実行され続けます。n = yield r
の行は非常に重要です。実行がこの行に到達すると、consume
関数の実行フローは一時停止し、r
の値を呼び出し元(つまり、produce
関数)に返し、関数の現在の状態を保存します。
produce
関数は、next(c)
を介して consume
コルーチンを開始し、独自の while
ループに入ります。各ループで、データのピース(n
)を生成し、c.send(n)
を介してデータを consume
コルーチンに送信します。c.send(n)
は、データを consume
コルーチンに送信するだけでなく、consume
コルーチンの実行を再開し、n = yield r
の行から続行させます。
2.3 実行結果と分析
実行結果:
[producer] Produced 1...
[consumer] Starting to consume 1...
[producer] Consumer return: 1 consumed
[producer] Produced 2...
[consumer] Starting to consume 2...
[producer] Consumer return: 2 consumed
[producer] Produced 3...
[consumer] Starting to consume 3...
[producer] Consumer return: 3 consumed
[producer] Produced 4...
[consumer] Starting to consume 4...
[producer] Consumer return: 4 consumed
[producer] Produced 5...
[consumer] Starting to consume 5...
[producer] Consumer return: 5 consumed
コンシューマー consume
が n = yield r
まで実行されると、プロセスは一時停止し、CPU を呼び出し元 produce
に返します。この例では、consume
と produce
の while
ループが連携して、単純なイベントループ機能をシミュレートします。そして、yield
と send
メソッドは、タスクの一時停止と再開を実装します。
古いコルーチンの実装をまとめると、次のようになります。
- イベントループ:
while
ループコードを手動で記述して実装されます。この方法は比較的基本的なものですが、開発者はイベントループの原理をより深く理解できます。 - コードの一時停止と再開:
yield
ジェネレーターの特性を利用して実現されます。yield
は、関数の実行を一時停止するだけでなく、関数の状態を保存することもでき、再開時に一時停止した場所から関数を続行できます。
3. 新しいコルーチンのレビュー
3.1 イベントループに基づく強力なシステム
新しいコルーチンは、asyncio
、async
、await
などのキーワードに基づいて実装されており、イベントループメカニズムがその中核にあります。このメカニズムは、イベントループ、タスク管理、コールバックメカニズムなど、より強力で効率的な非同期プログラミング機能を提供します。
3.2 主要コンポーネントの機能分析
- asyncio: 新しいコルーチンを実行するための基盤となるイベントループを提供します。イベントループは、すべての非同期タスクを管理し、それらの実行をスケジュールし、各タスクが適切なタイミングで実行されるようにする役割を担います。
- async: 関数をコルーチン関数としてマークするために使用されます。関数が
async def
として定義されると、コルーチンになり、await
キーワードを使用して非同期操作を処理できます。 - await: プロセスを一時停止する機能を提供します。コルーチン関数では、
await
が実行されると、現在のコルーチンの実行が一時停止し、await
に続く非同期操作の完了を待ってから、実行を再開します。
3.3 コード例の詳細な説明
import asyncio async def coro1(): print("start coro1") await asyncio.sleep(2) print("end coro1") async def coro2(): print("start coro2") await asyncio.sleep(1) print("end coro2") # イベントループを作成します loop = asyncio.get_event_loop() # タスクを作成します task1 = loop.create_task(coro1()) task2 = loop.create_task(coro2()) # コルーチンを実行します loop.run_until_complete(asyncio.gather(task1, task2)) # イベントループを閉じます loop.close()
この例では、2 つのコルーチン関数 coro1
と coro2
が定義されています。「start coro1」を出力した後、coro1
関数は await asyncio.sleep(2)
を介して 2 秒間一時停止します。ここで、await
は coro1
の実行を中断し、CPU をイベントループに返します。イベントループは、これらの 2 秒以内に coro2
などの他の実行可能なタスクをスケジュールします。「start coro2」を出力した後、coro2
関数は await asyncio.sleep(1)
を介して 1 秒間一時停止し、「end coro2」を出力します。coro2
が一時停止しているときに、イベントループに他の実行可能なタスクがない場合は、coro2
の一時停止時間が終了するのを待ってから、coro2
の残りのコードの実行を続行します。coro1
と coro2
の両方が実行されると、イベントループは終了します。
3.4 実行結果と分析
結果:
start coro1
start coro2
end coro2
end coro1
coro1
が await asyncio.sleep(2)
まで実行されると、プロセスは中断され、CPU はイベントループに返され、イベントループの次のスケジューリングを待ちます。この時点で、イベントループは coro2
をスケジュールして実行を続行します。
新しいコルーチンの実装をまとめると、次のようになります。
- イベントループ:
asyncio
によって提供されるloop
を介して実現され、これはより効率的で柔軟性があり、多数の非同期タスクを管理できます。 - プログラムの一時停止:
await
キーワードを介して実現され、コルーチンの非同期操作をより直感的で理解しやすくします。
4. 新旧のコルーチン実装の比較
4.1 実装メカニズムの違い
- yield: ジェネレーター (Generator) 関数のキーワードです。関数に
yield
ステートメントが含まれている場合、ジェネレーターオブジェクトを返します。ジェネレーターオブジェクトは、next()
メソッドを呼び出すか、for
ループを使用して、ジェネレーター関数内の値を取得するために段階的に反復処理できます。yield
を介して、関数を複数のコードブロックに分割し、これらのブロック間で実行を切り替えることができるため、関数の実行の一時停止と再開を実現できます。 - asyncio: Python が提供する非同期コードを記述するための標準ライブラリです。イベントループ (Event Loop) パターンに基づいており、単一のスレッドで複数の同時実行タスクを処理できます。
asyncio
は、async
およびawait
キーワードを使用してコルーチン関数を定義します。コルーチン関数では、await
キーワードを使用して現在のコルーチンの実行を一時停止し、非同期操作の完了を待ち、実行を再開します。
4.2 相違点のまとめ
- 古いコルーチン: 主に関数の
yield
キーワードが持つ、実行を一時停止および再開する機能を通じてコルーチンを実現します。その利点は、ジェネレーターに精通している開発者にとって、ジェネレーター構文に基づいて理解しやすいことです。その欠点は、ジェネレーターの概念と混同しやすく、イベントループを手動で記述する方法が十分に柔軟で効率的ではないことです。 - 新しいコルーチン: イベントループメカニズムと、
await
キーワードが持つプロセスを中断する機能を組み合わせて、コルーチンを実現します。その利点は、より強力で柔軟な非同期プログラミング機能を提供し、コード構造がより明確になり、最新の非同期プログラミングのニーズをより適切に満たすことです。その欠点は、初心者にとって、イベントループと非同期プログラミングの概念が比較的抽象的であり、理解して習得するのに時間がかかる場合があることです。
5. await
と yield
の関係
5.1 類似点
- 制御フローの一時停止と再開:
await
とyield
の両方に、特定の時点でコードの実行を一時停止し、後で続行する機能があります。この特性は、非同期プログラミングとジェネレータープログラミングで重要な役割を果たします。 - コルーチンのサポート: どちらもコルーチン (Coroutine) と密接に関連しています。これらを使用してコルーチンを定義および管理できるため、非同期コードの記述がより簡単で読みやすくなります。古いコルーチンでも新しいコルーチンでも、これらの 2 つのキーワードに依存してコルーチンのコア機能を実現します。
5.2 相違点
- 構文の違い:
await
キーワードは Python 3.5 で導入され、非同期関数での実行を一時停止し、非同期操作の完了を待つために特に使用されます。yield
キーワードは、初期のコルーチン用であり、主にジェネレーター (Generator) 関数で使用され、イテレーターを作成し、遅延評価を実装します。初期のコルーチンは、ジェネレーターの機能を通じて実現されました。 - セマンティクス:
await
は、現在のコルーチンが非同期操作の完了を待機し、実行を一時停止する必要があることを意味し、他のタスクに実行の機会を与えます。非同期操作の結果を待機することを強調し、非同期プログラミングにおける待機メカニズムです。yield
は、関数の状態を保存しながら、実行の制御を呼び出し元に渡し、次のイテレーションで一時停止した位置から実行を再開できるようにします。関数の実行の制御と状態の保存に重点を置いています。await
はプログラムを中断し、イベントループに新しいタスクをスケジュールさせます。yield
はプログラムを中断し、呼び出し元からの次の命令を待ちます。
- コンテキスト:
await
は、非同期関数やasync with
ブロックなど、非同期コンテキストで使用する必要があります。一方、yield
は、コルーチンを使用するコンテキストがなくても、関数がジェネレーター関数として定義されている限り、通常の関数で使用できます。 - 戻り値:
yield
はジェネレーターオブジェクトを返し、ジェネレーター内の値は、next()
メソッドを呼び出すか、for
ループを使用して段階的に反復処理できます。await
キーワードは、Future
、Task
、Coroutine
など、非同期操作の結果または状態を表す awaitable オブジェクト (Awaitable) を返します。
5.3 まとめ
await
は、yield
を介してプログラムの一時停止と実行を実装しません。これらには同様の機能がありますが、まったく呼び出し関係はなく、どちらも Python のキーワードです。await
は非同期プログラミングシナリオに適しており、非同期操作の完了を待つために使用され、より柔軟なコルーチン管理をサポートします。一方、yield
は主にジェネレーター関数で使用され、イテレーターと遅延評価を実装します。それらの適用シナリオと構文にはいくつかの違いがありますが、どちらも制御フローを一時停止および再開する機能を提供します。
Python コルーチンの開発プロセスをレビューし、新旧のコルーチン実装方法を比較し、await
と yield
の関係を詳細に分析することで、Python コルーチンの原則をより包括的かつ詳細に理解できます。これらの内容は理解するのが難しいですが、それらを習得することで、Python 非同期プログラミングの分野での探求のための確固たる基盤が築かれます。
Leapcell: Python アプリのホスティングに最適なサーバーレスプラットフォーム
最後に、Python アプリケーションのデプロイに最適なプラットフォームである Leapcell をご紹介します。
1. マルチ言語のサポート
- JavaScript、Python、Go、または Rust で開発できます。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ料金が発生します。リクエストや料金はかかりません。
3. 無敵の費用対効果
- アイドル料金なしの従量課金制。
- 例: 25 ドルで平均応答時間 60 ミリ秒で 694 万件のリクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的な UI。
- 完全に自動化された CI/CD パイプラインと GitOps の統合。
- 実用的な洞察を得るためのリアルタイムのメトリックとロギング。
5. 容易なスケーラビリティと高性能
- 容易に高い並行性を処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロです。構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ