PythonProgrammingPython Developer

**Python**のコルーチンオブジェクトが完了まで待機された後に再起動できない理由は何ですか?対照的に、ジェネレータ関数は各呼び出しごとに新しいイテレータを生成しますが。

Hintsage AIアシスタントで面接を突破

質問への回答

Pythonでは、async defを使用して作成されたコルーチンは、CPythonのバイトコードレベルでCO_ITERABLE_COROUTINEまたはネイティブコルーチンフラグによって管理される一度限りのステートマシンとして実装されています。非同期関数を呼び出すと、フレームオブジェクトと実行状態を含むコルーチンオブジェクトが即座に返され、これを待機することでこのステートマシンが完了に達し、その時点で内部のf_lasti(最後の命令)マーカーが終了に到達し、フレームが尽きたとマークされます。Pythonのランタイムは、この完了フラグをチェックすることで再エントリーを明示的に防ぎ、次の待機が発生した場合にはRuntimeErrorを発生させます。なぜなら、コルーチンは順次の制御フローを持つ単一の離散的非同期操作を表すように設計されているからです。対照的に、ジェネレータ関数はファクトリーです。各呼び出しは独立したスタックフレームと命令ポインタを持つ新しいPyGenObjectを作成し、機能が複数の独立したイテレータを生成できるようにします。それぞれが独自の実行コンテキストを維持します。

実生活の状況

ある開発チームは、指数バックオフを使用して失敗した接続試行を再試行する堅牢なWebSocketクライアントを構築していました。彼らは最初にモジュールレベルで接続コルーチンを定義し、再試行ロジックで再利用しようとしました。

import asyncio async def establish_connection(): return await websockets.connect("wss://api.example.com") # モジュールレベルのインスタンス化 connection_coro = establish_connection() async def retry_connect(max_attempts=3): for attempt in range(max_attempts): try: ws = await connection_coro # 2回目のイテレーションで失敗 return ws except Exception: await asyncio.sleep(2 ** attempt)

問題は、2回目のループイテレーションが再度connection_coroを待機しようとしたときに発生し、最初の成功した待機によってすでにコルーチンオブジェクトが消耗されたためにRuntimeErrorがトリガーされました。チームは3つのアーキテクチャソリューションを検討しました。

一つのアプローチは、RuntimeErrorをキャッチした後にexceptブロック内でコルーチンオブジェクトを手動で再構築することでした。技術的には可能でしたが、これは脆弱な状態管理をもたらし、コードが例外処理を通じて消耗を検出することに依存することとなり、意味的にあいまいで接続ロジック内の正当なランタイムエラーを隠す可能性がありました。

別の解決策として、establish_connection__await__を実装したクラスに変換してリセット可能な待機オブジェクトを作成することが提案されました。これによりファクトリーパターンが提供されましたが、不必要なボイラープレートと複雑さが増し、接続を確立するという単純な意図が曖昧になり、Pythonのランタイムが関数呼び出しを通じて提供しているものを複製する手動状態追跡が必要になりました。

選ばれた解決策は、async関数をファクトリーとして扱うことで、コールサイトをループ内に移動し、各イテレーションで新鮮なコルーチンオブジェクトが取得されることを確実にしました。ws = await establish_connection()にリファクタリングすることで、各試行が独自のリソース管理を持つ新しいステートマシンをインスタンス化しました。これは、非同期関数が一度限りの計算未来のコンストラクタとして設計されているというPythonのデザイン哲学と一致し、失敗した接続試行とその後の再試行を適切に隔離するクリーンで例外のない再試行ロジックを実現しました。

候補者が見逃しがちな点

コルーチンを変数に格納し、待機を忘れるとリソースリークが発生するのはなぜであり、close()がこれを軽減する方法は何ですか?

候補者は、待機されなかったコルーチンが単純にガーベジコレクトされ、副作用がないと仮定することがよくあります。しかし、コルーチンがその本体に入り、await表現(たとえば、データベース接続やロックを保持している場合)で一時停止している場合、フレームはこれらのリソースへの参照を保持します。コルーチンオブジェクトでclose()を呼び出すと、フレームを通じてGeneratorExit例外が発生し、コンテキストマネージャ(async with)およびtry/finallyブロックが即座にリソースを解放することを引き起こします。明示的なclose()がないと、これらのリソースは循環ガーベジコレクションが実行されるまで保持され続け、接続プールの枯渇シナリオでは遅すぎる場合があります。

inspect.iscoroutine()inspect.isawaitable()とどのように異なり、一般的なasyncioユーティリティを作成する際にこの区別が重要な理由は何ですか?

inspect.iscoroutine()は、async def関数によって作成されたネイティブコルーチンオブジェクトに対してのみTrueを返しますが、inspect.isawaitable()__await__を実装した任意のオブジェクト、すなわちコルーチン、タスク、フューチャー、カスタム待機オブジェクトに対してTrueを返します。候補者は、asyncioensure_future()関数がコルーチンだけでなく任意の待機可能オブジェクトも受け入れることを見逃します。iscoroutine()を厳密にチェックするライブラリを書くと、asyncio.Queue().get()やカスタムフューチャーオブジェクトのような有効な待機可能オブジェクトが却下され、任意の非同期操作をスケジュールするために設計された汎用ユーティリティ関数の多態性が破壊されます。

async forawaitの違いは何であり、非同期ジェネレータを消費する場合、前者がコルーチンではなくジェネレータ自体を返すために__aiter__を必要とするのはなぜですか?

awaitはコルーチンまたはフューチャーを完了まで消費し、単一の値を返しますが、async forは非同期イテレータを反復し、async defジェネレータ関数内の各yieldで一時停止します。候補者はasync forをコルーチンのリストを待機することと混同します。重要なことに、__aiter__は非同期イテレータオブジェクト自体を直接返さなければなりません(待機可能ではありません)。なぜなら、Pythonのランタイムは非同期反復プロトコルを開始する前にイテレータを取得するために__aiter__を同期的に呼び出すからです。__aiter__からコルーチンを返すとTypeErrorが発生し、プロトコルが非同期反復ステートマシンを駆動するためにイテレータの__anext__メソッドへの即時アクセスを期待するためです。