CPython(Pythonのリファレンス実装)では、locals()の動作は最適化戦略によって実行スコープに基づいて異なります。モジュールレベルでは、locals()は変数のための権威あるストレージであるグローバル名前空間辞書自体を返すため、変更はすぐに環境に反映されます。しかし、関数内部では、CPythonは「ファストローカル」という最適化を使用して、バイトコードによってインデックス付けされた固定サイズのC配列のPyObject*ポインタに変数を格納します。関数内でlocals()が呼ばれると、CPythonは新しい辞書を作成し、このファストローカル配列から値をコピーして埋め込み、一時的なスナップショットを生成します。したがって、この辞書への書き込みは一時的なマッピングのみを更新し、基盤となるファストローカル配列は変更されず、関数は元の変数の値を使用し続けます。
開発チームは、開発者がリモートデバッガインターフェースを介して実行中の関数のスコープの中に一時的なユーティリティ変数を注入できる動的デバッグツールを構築していました。最初の実装では、ブレークポイントでlocals()をキャプチャし、返された辞書にヘルパーオブジェクトを注入し、実行中の関数がその後の行でこれらのヘルパーにアクセスすることを期待しました。
最初のアプローチは、locals()によって返された辞書を直接変更しようとし、関数の名前空間へのライブ参照であると仮定しました。 利点: 関数のシグネチャに変更が不要で、構文的にシンプルに見えました。 欠点: CPythonはこの辞書をファストローカル配列の読み取り専用スナップショットとして扱うため、変更は捨てられ、実際のローカル変数は変更されませんでした。
2番目の戦略は、一時的な状態をglobals()に注入することでした。グローバル名前空間を共有の掲示板として使用します。 利点: この方法はアプリケーション全体でデータが持続し、引数を渡さずにどこからでもアクセスできました。 欠点: これは厳しいスレッドセーフの危険をもたらし、一時的なデバッグデータでグローバル名前空間を汚染し、内部状態がプロセス全体に公開されることでカプセル化の原則を侵害しました。
最終的な解決策は、計測された関数を明示的なcontext辞書引数を受け取るようにリファクタリングし、デバッガが可変状態を渡せるようにしました。 利点: このアプローチは明示的で、スレッドセーフであり、CPython、PyPy、Jythonのすべてで同様に機能し、明示的であることが暗黙のものであるよりも良いというPythonの原則に従っています。 欠点: これは対象となる関数のシグネチャと呼び出し元を変更する必要があり、他のアプローチよりも多くの初期リファクタリングを含みました。
チームは明示的なcontext渡し戦略を採用しました。これにより、CPython固有の実装の詳細への依存が排除され、名前空間の汚染が防止され、安定したクロスプラットフォームのデバッグユーティリティが得られました。
なぜlocals()はリスト内包表記内で通常のforループとは異なる動作をするのですか?
Python 3では、リスト内包表記は、ループ変数が周囲の名前空間に漏れないようにするために、ネストされた関数と同様に独自のローカルスコープを導入します。内包表記内でlocals()が呼ばれると、これはこの一時的スコープの辞書を返し、囲んでいる関数やモジュールではありません。さらに、通常の関数と同様に、この辞書は別々のコードオブジェクトとして実装された場合、ファストローカルからのスナップショットであるため、そこへの書き込みは持続しません。対照的に、モジュールレベルでは、locals()は実行中のモジュール辞書であるglobals()のエイリアスです。この区別は重要です。なぜなら、開発者はしばしば内包表記がその含まれるブロックと同じローカル名前空間を共有していると仮定し、その内部で変数をデバッグまたは注入しようとする際に混乱を招くからです。
sys._getframe()を介してフレームオブジェクトを操作することでファストローカルへの書き戻しを強制できるのでしょうか、リスクは何ですか?
高度なユーザーは、sys._getframe()を使用して現在の実行フレームにアクセスし、CPythonが書き込み可能なマッピングとして公開するframe.f_localsを変更できます。一部のバージョンでは、frame.f_localsへの代入は、PyFrame_LocalsToFastのような内部APIを使用してファストローカル配列への書き戻しを引き起こすことがありますが、この動作は実装依存でバージョンに脆弱であり、言語仕様の一部ではありません。リスクには、参照カウントが適切に管理されていない場合のメモリ破損、オプティマイザがすでにレジスタや配列にキャッシュしているために更新された値を無視する不一致な動作、全くファストローカル配列アーキテクチャを使用していない他のPython実装(例えばPyPy)での完全な失敗が含まれます。この技術に依存することは、未定義の動作を導入し、Pythonのバージョンをまたいでメインテナンス不可能なコードをもたらします。
明示的なローカルを伴うexec()またはeval()の存在が関数内のファストローカルの最適化にどのように影響しますか?
関数本体にローカル名前空間を参照するexec()またはeval()の呼び出しが含まれている場合、CPythonは変数が最適化されたファストローカル配列を介してのみアクセスされることを保証できません。実行される文字列は、動的に変数を導入または削除する可能性があります。これに対応するため、コンパイラはその関数のファストローカル最適化を無効にし、すべてのローカル変数を標準辞書に格納するフォールバックを行います。この「最適化されていない」モードでは、locals()はこの実際の辞書を返し、変更がすぐに持続するライブで可変のビューを提供します。これがexec()を使用するコードがしばしば遅く実行される理由であり、なぜそのような関数内でlocals()が"正しく"機能しているように見える(書き込みを許可する)理由であり、最適化された関数ではそうではないのです。