●Pythonの非同期処理とは?初心者でもわかる基礎知識
「非同期って何?」と思う方も多いはず。
心配ありません。
今回は、初心者の方にも理解しやすいように、非同期処理の基礎知識をお伝えします。
まずは、非同期処理の定義から始めましょう。
非同期処理とは、複数の処理を同時並行で行う手法のことを指します。
従来の同期処理では、一つの処理が終わるまで次の処理に進めませんでした。
しかし、非同期処理を使えば、ある処理が完了するのを待つ間に、別の処理を進めることができるのです。
例えば、料理をする場合を想像してみてください。
同期処理だと、野菜を切り終わってから肉を焼き、それが終わってからソースを作る、といった具合に一つずつタスクをこなします。
一方、非同期処理では、野菜を切りながらオーブンで肉を焼き、その間にソースも作れるのです。
○同期処理vs非同期処理
同期処理と非同期処理の違いをもう少し詳しく見ていきましょう。
同期処理は、タスクを順番に一つずつ実行します。
簡単で直感的ですが、時間がかかる処理があると、全体の実行時間が長くなってしまいます。
反対に、非同期処理では複数のタスクを並行して進められます。
時間のかかる処理があっても、その間に他の処理を行えるため、全体の実行時間を短縮できるのです。
特に、I/O処理(ファイルの読み書きやネットワーク通信など)が多い場合に効果を発揮します。
非同期処理の利点は、効率的なリソース利用です。
CPUの待機時間を減らし、システム全体のパフォーマンスを向上させられます。
また、ユーザー体験の向上にも繋がります。
例えば、Webアプリケーションで複数の画像を同時にダウンロードする際、非同期処理を使えば待ち時間を大幅に削減できるのです。
○なぜPythonで非同期処理が重要なのか?
Pythonは、その簡潔さと豊富なライブラリで人気のプログラミング言語です。
しかし、従来のPythonには「GIL(Global Interpreter Lock)」という仕組みがあり、マルチコアCPUの性能を十分に活用できないという課題がありました。
非同期処理は、この制限を克服する有効な手段となります。
特に、I/O負荷の高いアプリケーション(Webスクレイピングや大規模なデータ処理など)で威力を発揮します。
Pythonの非同期フレームワーク「asyncio」を使用することで、効率的で高速なプログラムを書くことができるのです。
また、最近のWeb開発では、リアルタイム性の高いアプリケーションが求められています。
チャットアプリや株価表示システムなど、データの即時更新が必要なケースが増えています。
非同期処理は、このようなリアルタイムアプリケーションの開発に欠かせない技術となっているのです。
●Python非同期処理の基本的な書き方
Pythonで非同期処理を実装する方法を理解しておきましょう。
基本的な書き方を理解すれば、複雑な非同期プログラムも怖くありません。
ここでは、Pythonの標準ライブラリ「asyncio」を使用した非同期処理の基本を解説します。
○asyncioライブラリの導入方法
asyncioは、Python 3.4以降に標準で組み込まれているライブラリです。
別途インストールの必要はありません。使用する際は、プログラムの先頭で次のようにインポートします。
このライブラリは、非同期プログラミングのための強力なツールキットを提供します。
イベントループ、コルーチン、タスクなど、非同期処理に必要な要素が揃っています。
○async/await構文の使い方
Pythonの非同期処理で中心的な役割を果たすのが、async/await構文です。
この構文を使うことで、非同期関数(コルーチン)を定義し、実行することができます。
async/await構文の基本的な使い方は次の通りです。
- 非同期関数(コルーチン)の定義には、def の前に async キーワードを付けます。
- 非同期関数内で、時間のかかる処理の前に await キーワードを使用します。
この例では、greet 関数が非同期関数として定義されています。
asyncio.sleep(1) で1秒の待機を模擬していますが、実際のプログラムではここにI/O処理などが入ります。
main 関数も非同期関数として定義され、greet 関数を2回呼び出しています。
最後に、asyncio.run() を使ってメインの非同期関数を実行します。
○サンプルコード1:シンプルな非同期関数の実装
より実践的な例として、複数のWebサイトから同時にデータを取得する非同期関数を実装してみましょう。
この例では、aiohttp ライブラリを使用します。
まず、aiohttp をインストールする必要があります。
そして、次のコードを実行します。
このコードは、3つのGitHub APIエンドポイントから同時にデータを取得します。
fetch_url 関数は非同期関数として定義され、URLからデータを取得します。
main関数では、asyncio.gather() を使用して複数のタスクを同時に実行しています。
実行結果は次のようになります。
非同期処理を使用することで、3つのURLからのデータ取得を0.53秒で完了しています。
同期処理で行った場合、各リクエストに1秒以上かかると仮定すると、全体で3秒以上かかる可能性があります。
このサンプルコードから、非同期処理が並行してタスクを実行し、全体の実行時間を大幅に短縮できることがわかります。
特に、I/O負荷の高い操作(この場合はネットワークリクエスト)で効果を発揮します。
●実践的なPython非同期処理テクニック
Pythonの非同期処理の基礎を押さえたところで、実践的なテクニックに踏み込んでいきましょう。
非同期処理の真価は、複雑な状況下で発揮されます。
複数のタスクを同時に実行したり、エラーを適切に処理したり、リソースを効率的に管理したりすることが求められるシーンで、非同期処理が真価を発揮するのです。
まずは、複数の非同期タスクを同時に実行する方法から見ていきましょう。
大規模なデータ処理や、多数のAPIリクエストを行う場合など、多くの現実世界のシナリオで役立つテクニックです。
○サンプルコード2:複数の非同期タスクを同時実行
複数の非同期タスクを同時に実行する場合、asyncio.gather()関数が非常に便利です。
この関数を使うと、複数のコルーチンを並行して実行し、全ての結果を一度に取得できます。
例えば、複数のWebサイトから同時にデータを取得するシナリオを考えてみましょう。
このコードでは、3つのGitHub APIエンドポイントから同時にデータを取得しています。
fetch関数は個々のURLからデータを取得し、fetch_all関数はasyncio.gather()を使って全てのfetchタスクを同時に実行します。
実行結果は次のようになります。
わずか0.54秒で3つのAPIからデータを取得できました。
同期処理だと、各リクエストに1秒以上かかるとすれば、全体で3秒以上かかっていたでしょう。
非同期処理の威力がよくわかりますね。
○サンプルコード3:非同期処理でのエラーハンドリング
非同期処理を行う上で、エラーハンドリングは非常に重要です。
ネットワークエラーやタイムアウトなど、予期せぬ問題が発生する可能性が高いためです。
Pythonの非同期処理では、try/except文を使ってエラーを捕捉できます。
ここでは、エラーハンドリングを組み込んだ非同期処理の例を紹介します。
このコードでは、存在しないURLを含めて3つのURLからデータを取得しようとしています。
fetch_url関数内でtry/except文を使用し、タイムアウトや接続エラーを捕捉しています。
実行結果は次のようになります。
エラーが発生しても、プログラムが停止せずに続行していることがわかります。
これで、一部のリクエストが失敗しても、他のリクエストは正常に処理できます。
○サンプルコード4:非同期コンテキストマネージャの活用
非同期コンテキストマネージャは、リソースの効率的な管理に役立ちます。
例えば、データベース接続やファイル操作など、開始時と終了時に特定の処理が必要な場合に便利です。
ここでは、非同期コンテキストマネージャを使用した例を紹介します。
このコードでは、AsyncTimerという非同期コンテキストマネージャを定義しています。
これは、処理の開始時と終了時に経過時間を表示します。
また、aiofilesライブラリを使用して、ファイルの非同期読み取りを行っています。
実行結果は次のようになります(ファイルの内容によって結果は異なります)。
3つのファイルが並行して処理され、各ファイルの処理時間が個別に表示されています。
非同期コンテキストマネージャを使うことで、リソースの管理が容易になり、コードの可読性も向上します。
●Python非同期処理の応用例
ここまで学んだ非同期処理の知識を、実際のシナリオに適用してみましょう。
Webスクレイピング、APIリクエスト、データベース操作など、非同期処理が真価を発揮する場面は多岐にわたります。
○サンプルコード5:非同期でのWebスクレイピング
Webスクレイピングは、多数のWebページから情報を抽出する作業です。
非同期処理を使うことで、複数のページを同時に取得し、処理時間を大幅に短縮できます。
ここでは、PythonのBeautifulSoup4とaiohttpを使用した非同期Webスクレイピングの例をみていきましょう。
このコードは、3つのPython関連のWebサイトから同時にタイトルを取得します。
get_title関数では、BeautifulSoup4を使ってHTMLを解析し、タイトルを抽出しています。
実行結果は次のようになります。
非同期処理により、3つのWebサイトからほぼ同時にデータを取得できました。
大量のWebページを処理する必要がある場合、この方法で処理時間を大幅に短縮できます。
○サンプルコード6:非同期APIリクエスト処理
APIリクエストも、非同期処理の恩恵を大きく受けられる領域です。
特に、複数のAPIエンドポイントにリクエストを送る場合、非同期処理で並行してリクエストを行うことで、全体の処理時間を短縮できます。
JSONPlaceholderの疑似APIを使用した非同期APIリクエストの例を紹介します。
このコードは、1人のユーザー情報と5つの投稿を同時に取得します。
fetch_user関数とfetch_post関数は、それぞれユーザー情報と投稿情報を非同期で取得します。
実行結果は次のようになります。
非同期処理により、6つの異なるAPIエンドポイントからデータを効率的に取得できました。
同期処理で行う場合と比べて、処理時間が大幅に短縮されています。
○サンプルコード7:非同期データベース操作
データベース操作も、非同期処理の恩恵を受けられる重要な領域です。
多数のクエリを実行する必要がある場合、非同期処理を使うことで、I/O待ち時間を有効活用し、全体の処理時間を短縮できます。
aiopgライブラリを使用したPostgreSQLデータベースへの非同期アクセスの例をみていきましょう。
このコードは、PostgreSQLデータベースに非同期でアクセスし、テーブルの作成、データの挿入、データの取得を行います。
create_table、insert_user、get_user関数は、それぞれテーブルの作成、ユーザーの挿入、ユーザーの取得を非同期で実行します。
main関数では、まずテーブルを作成し、1つのユーザーを挿入して取得します。
その後、5人のユーザーを一括で挿入します。
asyncio.gather()を使用することで、5つの挿入操作を並行して実行しています。
実行結果は次のようになります(実際のIDは環境によって異なります)。
非同期処理により、データベース操作を効率的に行うことができました。
特に、複数のユーザーを一括で挿入する際に、非同期処理の威力が発揮されています。
同期処理で行う場合と比べて、処理時間が大幅に短縮されているはずです。
●よくあるエラーと対処法
Pythonの非同期処理を学ぶ過程で、様々なエラーに遭遇することがあります。
初めて見るエラーメッセージに戸惑うこともあるでしょう。
よくあるエラーとその対処法を知っておけば、問題解決の手がかりになります。
まずは、非同期プログラミングで頻繁に遭遇する3つのエラーについて詳しく見ていきましょう。
○RuntimeError: This event loop is already running
非同期処理を行う際、イベントループが重要な役割を果たします。
しかし、時としてイベントループの管理に関するエラーが発生することがあります。
その代表例が「RuntimeError: This event loop is already running」です。
このエラーは、すでに実行中のイベントループ内で新たなイベントループを開始しようとした時に発生します。
例えば、Jupyterノートブックやipythonで非同期コードを実行する際によく見られます。
対処法としては、次のようなアプローチが効果的です。
- asyncio.run()の使用を避け、代わりにawaitを直接使用する
- nest_asyncioライブラリを使用する
具体的なコード例を見てみましょう。
このコードでは、nest_asyncioライブラリを使用してイベントループの入れ子を可能にしています。
これにより、「RuntimeError: This event loop is already running」エラーを回避できます。
○SyntaxError: ‘await’ outside async function
「SyntaxError: ‘await’ outside async function」は、非同期関数の外でawaitキーワードを使用した時に発生するエラーです。
Pythonの非同期処理では、awaitは必ずasync defで定義された関数内でのみ使用可能です。
このエラーは、非同期関数と同期関数の境界を混同した時によく起こります。
対処法は単純です。
awaitを使用する関数を必ずasync defで定義することです。
問題のあるコードと修正後のコードを比較してみましょう。
問題のあるコード
修正後のコード
この修正により、awaitキーワードが正しく非同期関数内で使用され、エラーが解消されます。
○AssertionError: Task got bad yield
「AssertionError: Task got bad yield」は、非同期関数内で不適切な値をyieldした時に発生するエラーです。
このエラーは、ジェネレータと非同期関数を混同した時によく起こります。
Pythonの非同期関数では、yieldステートメントの使用は特別な意味を持ちます。
単純なyieldは許可されておらず、yield fromかawaitを使用する必要があります。
問題のあるコードと修正後のコードを見比べてみましょう。
問題のあるコード
修正後のコード
修正後のコードでは、yieldの代わりにawaitを使用しています。
これで、非同期関数の正しい動作が保証され、エラーが解消されます。
●Pythonの非同期処理
基本的な非同期処理を理解した後は、より高度なテクニックを学ぶことで、非同期プログラミングの真の力を引き出すことができます。
ここでは、上級者向けの3つのテクニックを紹介します。
○非同期イテレータとasync forループの使い方
非同期イテレータは、データストリームを非同期的に処理する強力な方法です。
async forループと組み合わせることで、大量のデータを効率的に処理できます。
ここでは、非同期イテレータを使用して、複数のURLから順次データを取得する例を紹介します。
このコードでは、URLFetcherクラスが非同期イテレータとして機能します。
__aiter__と__anext__メソッドを実装することで、async forループで使用できるようになります。
○コルーチンとタスクの違いを理解する
コルーチンとタスクは、非同期プログラミングの基本的な構成要素ですが、その違いを理解することは重要です。
コルーチンは、async defで定義された関数です。
一方、タスクはコルーチンをラップし、イベントループ上での実行を管理するオブジェクトです。
次の例で、コルーチンとタスクの違いを見てみましょう。
このコードでは、my_coroutine()を直接awaitする方法と、タスクとして実行する方法の両方を表しています。
タスクを使用すると、複数のコルーチンを並行して実行する柔軟性が得られます。
○非同期コンテキストマネージャの作成方法
非同期コンテキストマネージャは、リソースの非同期的な獲得と解放を管理するための強力なツールです。
__aenter__と__aexit__メソッドを実装することで、独自の非同期コンテキストマネージャを作成できます。
ここでは、データベース接続を管理する非同期コンテキストマネージャの例を紹介します。
このコードでは、AsyncDatabaseConnectionクラスが非同期コンテキストマネージャとして機能します。
__aenter__メソッドでデータベース接続を確立し、__aexit__メソッドで接続を閉じます。
まとめ
Pythonの非同期処理は、効率的で高性能なプログラムを作成するための強力なツールです。
基本的な概念から始まり、実践的なテクニック、そして上級者向けの高度な使用法まで、幅広いトピックをカバーしてきました。
この記事で学んだ知識を活かし、実際のプロジェクトで非同期処理を積極的に活用してみてください。
エラーに遭遇しても、それを学びの機会と捉え、着実にスキルを向上させていくことが大切です。