27. 評価ループ
#27. 評価ループ
評価ループは、CPython の中心的な実行エンジンです。コンパイルされたコード オブジェクトを取得し、そのバイトコード命令を実行して、結果または例外を生成します。
大まかに言うと、CPython の実行は次のようになります。```text Python source ↓ tokens ↓ parser ↓ AST ↓ symbol table ↓ compiler ↓ code object ↓ frame ↓ evaluation loop ↓ Python result or exception
評価ループは CPython のインタプリタ実装内に存在します。歴史的に、キー ファイルは`Python/ceval.c`、周囲のインタプリタ機構が他のファイルに分散されています。最新の CPython は、バイトコード定義からいくつかのインタープリター コードも生成します。詳細はリリース間で変化しますが、モデルは安定しています。フレームはバイトコード命令を繰り返しディスパッチすることでコード オブジェクトを実行します。このコードはバージョン間で変更されるため、CPython 独自の開発者ガイドでは、現在の参照として内部ドキュメントとソース ツリーを示しています。
## 27.1 評価ループの仕事
評価ループは Python テキストを解析しません。 AST は構築されません。通常、語彙の範囲は決定されません。これらのジョブは、実行が開始されるまでにすでに終了しています。
その仕事はより狭く、より機械的です。```text
read the next bytecode instruction
decode its operand
perform the operation
update the frame
continue, jump, call, return, or raise
```たとえば、この関数は次のようになります。```python
def add(a, b):
return a + b
```コードオブジェクトにコンパイルされます。コードオブジェクトにはバイトコードが含まれています。いつ`add(2, 3)`が呼び出されると、CPython はその呼び出し用のフレームを作成または初期化し、引数を高速ローカル スロットに格納してから、評価ループを通じてフレームを実行します。
ループは最終的に return 命令に到達します。この命令は、戻り値をポップまたは読み取り、フレームを巻き戻して、オブジェクト ポインタを呼び出し元に返します。
概念的には:```text
call add(2, 3)
create frame
store a = 2
store b = 3
execute LOAD_FAST a
execute LOAD_FAST b
execute BINARY_OP +
execute RETURN_VALUE
return 5
```実際の実装はさらに複雑ですが、これが核となるモデルです。
## 27.2 主要なランタイムオブジェクト
評価ループは、複数の CPython ランタイム オブジェクトを接続します。
|ランタイムオブジェクト |役割 |
|---|---|
|コードオブジェクト |不変のコンパイル済みバイトコードとメタデータ |
|フレーム | 1 回の呼び出しで実行状態が変更可能 |
|スレッドの状態 |スレッドごとのインタープリターの実行状態 |
|インタプリタの状態 |インタプリタごとのランタイム状態 |
| Python オブジェクト |命令によって操作されるランタイム値 |
|型オブジェクト | Python オブジェクトの実行時動作テーブル |
コード オブジェクトは、実行すべき内容を記述します。
フレームには、そのコードの 1 つのアクティブな実行が保存されます。
評価ループはフレームを実行します。
この区別が重要です。単一のコード オブジェクトは何度も実行できます。各呼び出しは独自のフレーム状態を取得します。```python
def f(x):
return x + 1
a = f(10)
b = f(20)
```どちらの呼び出しも同じコード オブジェクトを使用しますが、各呼び出しには個別のローカル、スタック状態、および戻り値があります。
## 27.3 コードオブジェクト
コード オブジェクトには、Python コードのコンパイルされた表現が含まれます。
Python から検査することができます。```python
def f(x):
y = x + 1
return y
code = f.__code__
print(code.co_name)
print(code.co_varnames)
print(code.co_consts)
print(code.co_names)
print(code.co_stacksize)
```典型的なフィールドには次のものがあります。
|フィールド |意味 |
|---|---|
|`co_code`|バイトコード ストリーム、バージョン依存の形式で公開 |
|`co_consts`|コードで使用されるリテラル定数 |
|`co_names`|バイトコードによって参照される名前 |
|`co_varnames`|ローカル変数名 |
|`co_freevars`|外部スコープからキャプチャされた自由変数 |
|`co_cellvars`|内部スコープによってキャプチャされる変数 |
|`co_argcount`|位置引数の数 |
|`co_kwonlyargcount`|キーワードのみの引数の数 |
|`co_stacksize`|必要な値 スタック サイズ |
|`co_flags`|コードフラグ |
|`co_filename`|ソースファイル名 |
|`co_name`|関数またはブロック名 |
|`co_qualname`|修飾名 |
|`co_firstlineno`|最初のソース行 |
|ラインテーブル |バイトコード オフセットからソース行へのマッピング |
|例外テーブル |構造化例外処理メタデータ |
の`dis`モジュールは、CPython バイトコードを検査するために特別に存在します。そのドキュメントには、CPython バイトコードは実装の詳細であり、Python のバージョン間で変更される可能性があると記載されています。
## 27.4 フレーム
フレームは実行記録です。
関数が実行されるとき、CPython は以下を保存する場所が必要です。```text
the code object being executed
the instruction pointer
local variables
temporary stack values
globals dictionary
builtins dictionary
closure cells
exception state
return state
tracing and profiling state
```その構造物がフレームです。
簡略化されたフレーム モデルは次のようになります。```text
frame
code object
globals
builtins
locals / fast locals
instruction pointer
value stack
block and exception state
previous frame / caller relation
```Python 呼び出しチェーンはフレームのチェーンを作成します。```python
def a():
return b()
def b():
return c()
def c():
return 42
a()
```概念的には:```text
frame for a
frame for b
frame for c
```いつでも、現在のスレッド状態は、現在実行中のフレームまたは同等の内部フレーム表現を指します。
## 27.5 高速ローカル
関数のローカル変数は、通常、実行中に通常のディクショナリに格納されません。
CPython は、高速ローカル変数に配列のようなレイアウトを使用します。名前はコンパイル時にローカル インデックスに解決されます。これにより、バイトコード命令は、辞書検索を行う代わりに、インデックスによってローカル変数にアクセスできるようになります。
例:```python
def f(a, b):
c = a + b
return c
```コンパイラはローカル スロットを割り当てます。
|名前 |スロット |
|---|---:|
|`a`| 0 |
|`b`| 1 |
|`c`| 2 |
これにより、バイトコードはスロットベースの操作を使用できるようになります。```text
LOAD_FAST 0 load a
LOAD_FAST 1 load b
BINARY_OP +
STORE_FAST 2 store c
LOAD_FAST 2 load c
RETURN_VALUE
```これが、ローカル変数へのアクセスが一般的にグローバル変数へのアクセスよりも高速である理由です。ローカル アクセスでは、ダイレクト フレーム スロットを使用できます。グローバル アクセスでは、辞書を検索し、組み込みフォールバックを処理する必要があります。
## 27.6 値スタック
CPython バイトコードはスタック モデルを使用します。
ほとんどの命令は、フレームローカル値スタックからの読み取りとフレームローカル値スタックへの書き込みを行います。このスタックは C 呼び出しスタックとは別のものです。保管します`PyObject *`バイトコード実行時の値。
この式の場合:```python
x = (a + b) * c
```スタックの動作は大まかに次のとおりです。```text
LOAD_FAST a stack: [a]
LOAD_FAST b stack: [a, b]
BINARY_OP + stack: [a_plus_b]
LOAD_FAST c stack: [a_plus_b, c]
BINARY_OP * stack: [product]
STORE_FAST x stack: []
```値スタックはバイトコード設計の中心です。これにより、すべての命令でソースおよび宛先レジスタを明示的に指定する必要がなくなります。代わりに、命令はスタック効果について一致します。
一部の命令は値をプッシュします。```text
LOAD_CONST
LOAD_FAST
LOAD_GLOBAL
BUILD_LIST
```一部の命令は値をポップします。```text
STORE_FAST
POP_TOP
RETURN_VALUE
```両方を行う人もいます。```text
BINARY_OP
CALL
LOAD_ATTR
COMPARE_OP
```## 27.7 命令ポインタ
フレームは、バイトコード ストリーム内のどこで実行が行われているかを追跡します。
直線コードの場合、命令ポインタは各命令の後に前方に移動します。
分岐、ループ、例外処理、および戻りの場合、命令によって制御フローが変更されます。
例:```python
def sign(x):
if x < 0:
return -1
return 1
```概念的なバイトコード フロー:```text
load x
load 0
compare <
jump if false to positive_return
load -1
return
positive_return:
load 1
return
```命令ポインタはこれを可能にするものです。分岐命令は、次に実行する命令を変更します。
## 27.8 ディスパッチ
ディスパッチは、現在のバイトコード命令の C 実装を選択する行為です。
簡略化されたインタプリタ ループは次のようになります。```c
for (;;) {
opcode = read_opcode(frame);
oparg = read_operand(frame);
switch (opcode) {
case LOAD_FAST:
/* load local variable */
break;
case LOAD_CONST:
/* load constant */
break;
case BINARY_OP:
/* perform binary operation */
break;
case RETURN_VALUE:
/* return from frame */
break;
}
}
```これは単なる教育モデルです。最新の CPython は、最適化されたディスパッチ技術と生成されたインタープリター コードを所々に使用しています。それでも、基本的な形状は残っています。```text
fetch
decode
dispatch
execute
repeat
```派遣コストは重要です。すべての Python バイトコード命令はディスパッチを通過します。ループが何百万ものバイトコード命令を実行すると、ディスパッチのオーバーヘッドが目に見えて現れます。
## 27.9 スタック効果
すべてのバイトコード命令にはスタック効果があります。
スタック効果は、命令が消費および生成する値の数を表します。
たとえば:
|指示 |入力スタック |出力スタック |
|---|---|---|
|`LOAD_CONST` | `[]` | `[const]` |
| `LOAD_FAST` | `[]` | `[local]` |
| `STORE_FAST` | `[value]` | `[]` |
| `BINARY_OP` | `[left, right]` | `[result]` |
| `RETURN_VALUE` | `[value]`|フレームから戻ります |
コンパイラは、コード オブジェクトに必要な最大スタック サイズを計算するためにスタック効果を認識している必要があります。その値は次のように表示されます`co_stacksize`。
のために:```python
def f(a, b, c):
return (a + b) * c
```正確なバイトコードに応じて、スタックは 2 つまたは 3 つを超える一時値を保持する必要はありません。 CPython は、フレームが十分なスペースを確保できるように、必要な最大スタック深さを記録します。
## 27.10 バイトコードオペランド
多くのバイトコード命令にはオペランドがあります。
オペランドは、命令に付加される小さな整数の引数です。意味はオペコードによって異なります。
例:```text
LOAD_CONST 0 load co_consts[0]
LOAD_FAST 1 load fast local slot 1
STORE_FAST 2 store into fast local slot 2
LOAD_GLOBAL 3 load name from name table index 3
```バイトコード命令は通常、オブジェクトまたは文字列名への完全なポインタを格納しません。コード オブジェクトが所有するテーブルにインデックスを格納します。
これにより、バイトコードがコンパクトに保たれ、不変のメタデータが実行状態から分離されます。
## 27.11 単純な関数の実行
次のことを考慮してください。```python
def add(a, b):
return a + b
```逆アセンブリは Python のバージョンによって異なる場合がありますが、概念的な命令シーケンスは次のとおりです。```text
load local a
load local b
binary add
return value
```実行は次のように進みます。
|ステップ |指示 |前にスタック | | の後にスタック
|---:|---|---|---|
| 1 |`LOAD_FAST a` | `[]` | `[a]`|
| 2 |`LOAD_FAST b` | `[a]` | `[a, b]`|
| 3 |`BINARY_OP +` | `[a, b]` | `[a + b]`|
| 4 |`RETURN_VALUE` | `[a + b]`|戻る |
C レベルでは、各スタック要素は`PyObject *`。
のために`add(2, 3)`、スタックは Python 整数オブジェクトへのポインターを保持します。追加操作は、Python オブジェクト セマンティクスを通じてディスパッチされます。一般的なケースでは、CPU 整数加算を直接出力しません。
## 27.12 なぜ`a + b`1 つの CPU 命令だけではない
Pythonでは、`a + b`ダイナミックです。
オブジェクトは整数である場合があります。```python
1 + 2
```文字列の場合もあります。```python
"hello " + "world"
```それらはリストである場合があります。```python
[1] + [2]
```これらはユーザー定義オブジェクトである場合があります。```python
class X:
def __add__(self, other):
return "custom"
X() + X()
```加算のバイトコード命令は、Python のデータ モデルを尊重する必要があります。オペランドの型を検査し、正しい数値演算またはシーケンス演算を見つけ、必要に応じて特別なメソッドを呼び出し、エラーを処理し、Python オブジェクトを返す必要があります。
したがって、評価ループでは処理できません。`+`プレーンマシン追加として。これは、Python オブジェクトに対する動的操作です。
最新の CPython では、可能な限りこのオーバーヘッドを軽減します。特殊化された適応型インタプリタは、安定した実行時の動作を観察した後、操作を特殊化できます。 PEP 659 では、これを、動作が変化したときに迅速に適応する小さな領域の特殊化として説明しています。
## 27.13 関数呼び出し
関数呼び出しは、評価ループ内で最も重要なパスの 1 つです。
のために:```python
result = f(x, y)
```通訳者は次のことを行う必要があります。```text
load callable f
load arguments x and y
arrange call arguments
check callable type
enter optimized call path if possible
create or initialize callee frame if it is a Python function
execute callee frame
receive return value
continue caller frame
```概念的には:```text
caller frame
LOAD_FAST f
LOAD_FAST x
LOAD_FAST y
CALL 2
create callee frame
run callee frame
return object
STORE_FAST result
```呼び出しは頻繁でコストがかかるため、CPython は呼び出しに多大な最適化努力を費やしてきました。重要なメカニズムには次のものがあります。```text
vectorcall
fast locals
specialized call bytecodes
inline caches
frame optimizations
reduced temporary tuple/dict creation
```目的は、不必要な引数のパッキングを避けることです。歴史的には、多くの呼び出しでは引数のタプルと辞書を構築する必要がありました。最新の呼び出しパスは、可能な限り配列のようなレイアウトで引数を渡そうとします。
## 27.14 フレームから戻る
return 命令は現在のフレームを終了します。
のために:```python
def f():
return 42
```return 命令は、`PyObject *`結果が表示され、フレームが巻き戻されます。
呼び出し元は、呼び出し式の結果としてそのオブジェクトを受け取ります。```python
x = f()
```概念的には:```text
callee frame stack: [42]
RETURN_VALUE
pop result
finish callee frame
give result to caller
caller resumes with stack: [42]
STORE_FAST x
```フレームはいくつかの方法で終了できます。
|出口パス |意味 |
|---|---|
|通常の復帰 |関数は値を返します |
|例外 | | を上げて関数を終了します。
|発電機の発電量 |フレームは一時停止し、後で再開します。
|コルーチンが待機中 |コルーチンが一時停止する |
|致命的なエラー |ランタイムレベルの障害 |
評価ループはこれらのパスをすべて処理する必要があります。
## 27.15 例外
例外は、通常のインタープリタ制御フローの一部です。
のために:```python
def div(a, b):
return a / b
```もし`b`がゼロの場合、除算演算により次の結果が得られます。`ZeroDivisionError`。
バイトコード命令は正常な結果を返しません。代わりに、例外状態を設定し、制御を例外処理ロジックに渡します。
概念的には:```text
execute BINARY_OP /
operation fails
set current exception
search exception table
jump to handler or unwind frame
```最新の CPython は、コード オブジェクトに関連付けられた構造化例外テーブルを使用します。これらの表では、保護されたバイトコード範囲とハンドラーについて説明します。これにより、例外が発生したときにインタープリタが正しいハンドラを見つけることができます。
例:```python
try:
x = 1 / y
except ZeroDivisionError:
x = 0
```評価ループは、保護範囲がどこにあるのか、ハンドラーがどこから開始されるのか、ハンドラーでどのようなスタック状態が必要なのかを認識している必要があります。
## 27.16 ループと分岐
Python ループはコンパイルしてジャンプします。
例:```python
def count(n):
i = 0
while i < n:
i += 1
return i
```概念的なバイトコードの形状:```text
i = 0
loop_start:
load i
load n
compare <
jump if false to loop_end
load i
load 1
add
store i
jump to loop_start
loop_end:
load i
return
```評価ループには、各 Python に対する特別な C レベルの while ループはありません。`while`。ループを実装するバイトコード命令を実行します。
したがって、Python ループは、外側のインタープリター ループ内のインタープリター ループになります。```text
C evaluation loop
executes Python loop bytecode
jumps backward many times
```これが、タイトな Python ループが高価になる理由の 1 つです。各反復では多くのバイトコード命令が実行される可能性があり、各バイトコード命令にはディスパッチと動的オブジェクトのオーバーヘッドがあります。
## 27.17 反復
あ`for`ループは反復プロトコルを使用します。
例:```python
for item in xs:
use(item)
```概念的な実行:```text
iterator = iter(xs)
loop:
item = next(iterator)
if StopIteration:
exit loop
use(item)
jump loop
```評価ループは次の命令を実行します。`iter()`、イテレータの次の操作を呼び出し、ハンドルします`StopIteration`、分岐します。
これはPythonレベルを意味します`for`ループはプロトコルベースです。インタプリタはオブジェクト プロトコル スロットを通じてディスパッチするため、これらはリスト、タプル、辞書、ファイル、ジェネレータ、カスタム イテレータ、および多くの拡張タイプに対して機能します。
## 27.18 属性アクセス
属性アクセスも動的です。
のために:```python
value = obj.name
```インタプリタは、Python の属性検索ルールを実装する必要があります。```text
look at object type
handle descriptors
look in instance dictionary if applicable
look in class dictionary and base classes
call custom __getattribute__ if present
fall back to __getattr__ if applicable
raise AttributeError if missing
```シンプルに見える式には、重要な機構が含まれる場合があります。
最新の CPython は、インライン キャッシュと特殊化を使用して、一般的な属性アクセス パターンを高速化します。たとえば、安定した形状を持つオブジェクトの同じ属性に繰り返しアクセスすると、繰り返しの検索作業を回避できます。
## 27.19 グローバルおよび組み込みルックアップ
グローバル検索はローカル検索よりも高価です。
のために:```python
print(len(xs))
```などの名前`print`そして`len`ローカルに割り当てられない限り、ローカル変数ではありません。 CPython は、グローバル名前空間と組み込み名前空間を通じてそれらを検索します。
概念的には:```text
look in globals dictionary
if missing, look in builtins dictionary
if missing, raise NameError
```これが、タイトなループでローカル バインディングの方が高速になる理由です。```python
def slow(xs):
for x in xs:
len(x)
def faster(xs):
local_len = len
for x in xs:
local_len(x)
```最新の CPython はグローバル ルックアップに特化できるため、この古いマイクロ最適化は以前ほど普遍的に有用ではなくなりました。それでも、根本的な違いは残ります。ローカル スロットは辞書ベースの名前検索よりも簡単です。
## 27.20 GIL と評価ループ
従来の CPython ランタイムでは、現在のスレッドがグローバル インタプリタ ロックを保持している間に評価ループが実行されます。
GIL は、参照カウントや多くのオブジェクト内部を含むインタープリターの状態を保護します。評価ループは、GIL の削除、シグナルの処理、保留中の呼び出しの処理、または別のスレッドの実行を許可する必要があるかどうかを定期的にチェックします。
これは、バイトコードの実行がインタープリター レベルで協調的に行われることを意味します。通常、スレッドは GIL を永久に保持するわけではありません。 CPython には、スレッド間の切り替えを可能にするスケジューリング チェックがあります。
実際的な結果:```text
one thread executes Python bytecode at a time per traditional interpreter
I/O operations may release the GIL
C extensions may release the GIL around long native work
CPU-bound Python threads do not normally execute bytecode in parallel
```新しい CPython の作業にはフリースレッドのビルドとインタープリターごとの変更が含まれていますが、評価ループは依然としてスレッドの状態、保留中の作業、およびバイトコードの実行が出会う中心的な場所です。
## 27.21 実行中の参照カウント
スタック上のすべての値は、所有権ルールを備えた Python オブジェクト ポインターです。
評価ループでは、参照カウントを注意深く維持する必要があります。命令が値をプッシュしたり、値を保存したり、値を置換したり、値を破棄したりする場合、オブジェクトの有効期間を正しく保存する必要があります。
例:```python
x = a + b
```概念的には:```text
load a obtain reference to object a
load b obtain reference to object b
add produce new reference to result
store x bind result to local slot
discard temporaries
```不適切な参照管理は、漏洩または早期の破壊を引き起こす可能性があります。
C レベルでは、これは以下に相当する操作を慎重に配置することを意味します。```c
Py_INCREF(obj);
Py_DECREF(obj);
```正確な実装では、多くの場合、特殊なマクロと所有権規則が使用されます。しかし、不変条件は単純です。オブジェクトは使用できる限り生き続けなければならず、インタプリタが参照を所有しなくなった場合には解放されなければなりません。
## 27.22 エラー通知
CPython のほとんどの C ヘルパー関数は、次のような共通の規則を使用します。```text
return a valid pointer or success code on success
return NULL or error code on failure
set an exception on failure
```評価ループはこれらの結果をチェックします。
簡略化した例:```c
PyObject *result = PyNumber_Add(left, right);
if (result == NULL) {
goto error;
}
```の`NULL`return 自体は例外を説明しません。例外はスレッド状態に保存されます。
このパターンはどこにでも現れます。```text
call helper
if failed:
go to error path
else:
push or store result
```ほぼすべての Python 操作が失敗する可能性があるため、評価ループには多くのエラー終了が含まれます。```text
allocation can fail
attribute lookup can fail
function call can fail
comparison can fail
iteration can fail
import can fail
descriptor code can fail
user-defined special method can fail
```## 27.23 保留中の呼び出し、シグナル、および非同期イベント
評価ループは、ランタイムレベルの作業の安全なチェックポイントとしても機能します。
CPython は、任意の C 命令境界ですべての信号または保留中のイベントを処理することはできません。代わりに、何かに注意が必要であることを記録し、評価中に管理されたポイントでチェックします。
例:```text
signal handling
pending calls from C APIs
thread switching requests
async exception injection
tracing and profiling hooks
monitoring hooks
interrupt checks
```これにより、インタプリタが管理しやすくなります。評価ループは、Python の実行が外部イベントを認識する場所になります。
## 27.24 トレースとプロファイリング
Python は、次のような API を介したトレースとプロファイリングをサポートしています。```python
sys.settrace(...)
sys.setprofile(...)
```これらのフックには、評価ループからの協力が必要です。
ループは次のようなイベントを発行する必要があります。```text
call
line
return
exception
opcode, when enabled
```トレースするとチェックとコールバック呼び出しが追加されるため、実行が遅くなります。ただし、デバッガー、カバレッジ ツール、プロファイラー、教育ツール、可観測性システムが可能になります。
Python コードをステップ実行するデバッガーは、バイトコードの実行をソース行にマッピングする評価ループの機能に依存します。
## 27.25 適応型インタプリタの特化
Python 3.11 以降、CPython には PEP 659 に基づく特殊な適応型インタープリターが組み込まれています。そのアイデアは、Python のセマンティクスを動的に保ちながら、一般的な安定したケースを高速化することです。 PEP 659 では、実行時パターンが変化した場合に適応する、小さな領域に対する積極的な特殊化について説明しています。
インタプリタは一般的なバイトコードから始まります。コードが実行されると、CPython は動作を観察し、一般的な操作を特殊な形式で置き換えたり拡張したりする場合があります。
たとえば、一般的な 2 項演算は、一般的なオペランド タイプに対して最適化される場合があります。```text
generic BINARY_OP
observed int + int repeatedly
↓
specialized integer-add path
```属性アクセスの場合:```text
generic LOAD_ATTR
observed same attribute layout repeatedly
↓
cached attribute access path
```グローバル検索の場合:```text
generic LOAD_GLOBAL
observed stable globals and builtins dictionaries
↓
cached global lookup path
```専門化は正しいままでなければなりません。仮定が失敗した場合、インタプリタはフォールバックするか適応します。
これは、従来の完全な JIT コンパイラーとは異なります。これは依然としてインタープリタ アーキテクチャ内で動作します。一般的に関数全体をネイティブ マシン コードにコンパイルするのではなく、バイトコード レベルの実行パスに特化します。
## 27.26 インラインキャッシュ
インライン キャッシュは、バイトコード命令に関連付けられた小さなキャッシュ ストレージです。
インタプリタは、ルックアップ情報を毎回再計算するのではなく、ファクトを必要とする命令の近くにファクトを保存します。
キャッシュ情報の例には次のものが含まれます。```text
type version
dictionary version
attribute offset
resolved descriptor
global dictionary version
builtin dictionary version
specialized call target
```簡略化された属性キャッシュ モデル:```text
LOAD_ATTR name
cache:
expected type = User
type version = 123
attribute offset = 2
```次回の実行時に、CPython はオブジェクトがキャッシュされた仮定と依然として一致するかどうかを確認できます。 「はい」の場合、高速パスが使用されます。 「いいえ」の場合は、汎用パスに戻ります。
インライン キャッシュは、特定のソース位置でのバイトコード命令が同じ種類のオブジェクトを繰り返し参照することが多いため、適切に機能します。
## 27.27 特化が安全な理由
Python は動的であるため、特殊化を保護する必要があります。
特殊化されたパスは、その前提が真実である場合にのみ有効です。
例えば:```python
obj.x
```CPython が安定したオブジェクト レイアウトを観察する場合は、特殊化することができます。しかし、Python ではミューテーションが可能です。```python
obj.__dict__["x"] = 10
type(obj).x = property(...)
obj.__class__ = OtherType
```そのため、CPython はバージョン タグ、ガード、カウンター、フォールバック パスを使用します。
安全規則は次のとおりです。```text
use fast path only if guards prove assumptions still hold
otherwise use generic Python semantics
```これは、多くの動的言語ランタイムで使用されているものと同じ広範な戦略ですが、CPython では機構がバイトコード インタープリターに比較的近い位置に保たれています。
## 27.28 生成されたインタプリタコード
最新の CPython は、すべてのバイトコード実装を 1 つのファイル内の手書きのスイッチ ケースとして扱いません。
インタプリタの一部は命令定義から生成されます。これは、バイトコードのメタデータ、スタック効果、特殊化情報、およびディスパッチ コードの一貫性を保つのに役立ちます。
大まかな考え方:```text
instruction definitions
↓
generated opcode metadata
↓
generated dispatch support
↓
interpreter execution
```これは、読者にとって、信頼できる情報源が、最終的に生成された C ファイルだけであるとは限らないことを意味します。多くの場合、命令定義ファイル、生成されたヘッダー、およびビルド出力を検査する必要があります。
正確なファイルと生成パイプラインは CPython リリースごとに異なる可能性があるため、調査しているバージョンのソース ツリーを使用してください。
## 27.29 評価ループと C 呼び出し
評価ループは C ヘルパー関数を頻繁に呼び出します。
例:```text
PyNumber_Add
PyObject_GetAttr
PyObject_SetAttr
PyObject_Call
PyDict_GetItem
PyObject_RichCompare
PyIter_Next
```これらのヘルパーは、ユーザー定義の Python コードを呼び出すことができます。
例えば:```python
a + b
```次のように呼び出すことができます:```python
a.__add__(b)
```そして:```python
obj.name
```次のように呼び出すことができます:```python
obj.__getattribute__("name")
```したがって、評価ループは間接的に Python の実行に再び入ることができます。バイトコード命令は C ヘルパー コードを呼び出すことができ、C ヘルパー コードは Python コードを呼び出すことができ、別のフレームを作成して別の評価ループの実行を開始します。
概念的には:```text
frame A
executes BINARY_OP
calls C helper
calls user __add__
frame B
evaluation loop
```この再帰的実行モデルは、Python の柔軟性の中心です。
## 27.30 再帰と呼び出しの深さ
Python は制御されない再帰から保護します。
例:```python
def f():
return f()
f()
```呼び出しごとに別の Python フレームが作成されます。 CPython は再帰の深さを追跡し、レイズします`RecursionError`when the configured limit is exceeded.
評価ループと呼び出し機構はこのチェックと連携する必要があります。これがないと、再帰的な Python コードが C スタックまたはプロセス メモリを使い果たす可能性があります。
You can inspect and adjust the limit:```python
import sys
print(sys.getrecursionlimit())
sys.setrecursionlimit(2000)
```再帰制限を上げる場合は慎重に行う必要があります。 Python の制限は、下位レベルのランタイム リソースを保護するために部分的に存在します。
## 27.31 ジェネレータ
ジェネレーターはフレームのライフサイクルを変更します。
通常の関数呼び出しは、返されるか呼び出されるまで実行されます。ジェネレーターは一時停止および再開できます。
例:```python
def gen():
yield 1
yield 2
```電話をかける`gen()`関数本体をすぐに実行して完了するわけではありません。一時停止されたフレームまたは同等の実行状態を所有するジェネレーター オブジェクトを作成します。
それぞれ`next()`実行を再開します:```text
first next()
enter frame
run until yield 1
suspend frame
second next()
resume frame
run until yield 2
suspend frame
third next()
resume frame
finish function
raise StopIteration
```評価ループは一時停止をサポートする必要があります。単にフレームを破壊することはできません`yield`。
## 27.32 コルーチンと Await
コルーチンは同じサスペンション モデルを拡張します。
例:```python
async def fetch():
data = await read()
return data
```アン`await`別の awaitable が完了するまでコルーチンを一時停止することがあります。
評価ループは以下をサポートする必要があります。```text
coroutine frame creation
suspension at await
resumption with value
resumption with exception
final return
cancellation behavior
```したがって、非同期実行は別個のインタープリターではありません。これは、一時停止と再開のための特定の命令とプロトコルを備えた同じフレームとバイトコード機構上に構築されています。
## 27.33 クラス本体とモジュール本体
評価ループは関数を実行するだけではありません。
また、モジュール本体とクラス本体も実行します。
モジュールファイル:```python
x = 1
def f():
return x
```モジュールレベルのコードオブジェクトにコンパイルされます。モジュールをインポートまたは実行すると、そのコード オブジェクトが実行されます。
class ステートメントはコードも実行します。```python
class C:
x = 1
def method(self):
return self.x
```クラス本体は、クラス構築用に用意された名前空間で実行されます。実行後、CPython はその名前空間からクラス オブジェクトを構築します。
したがって、評価ループはいくつかの種類のブロックを実行します。
|ブロックの種類 |例 |
|---|---|
|モジュール |`.py`ファイル本体 |
|機能 |`def f(): ...`|
|クラス本体 |`class C: ...`|
|ラムダ |`lambda x: x + 1`|
|理解 |`[x * 2 for x in xs]`|
|ジェネレーター |`(x for x in xs)`|
|コルーチン |`async def f(): ...`|
## 27.34 内包表記
多くの場合、内包表記は独自のコード オブジェクトにコンパイルされます。
例:```python
ys = [x * 2 for x in xs if x > 0]
```概念的には:```text
create list
iterate xs
for each x:
if x > 0:
append x * 2
return list
```これは、内包表記がネストされたフレームまたは特殊な内部実行パスを通じて実行されることが多いことを意味します。これらには独自のローカル スコープ動作があるため、Python 3 ではリスト内包表記内のループ変数が周囲のスコープにリークしません。
評価ループは、内包表記の実行を特別な構文形式としてではなく、バイトコードの実行として認識します。
## 27.35 インポートの実行
インポートも最終的にはバイトコードを実行します。
Python が`.py`モジュールを作成すると、インポート システムはモジュールを見つけ、ソースまたはキャッシュされたバイトコードを読み取り、モジュール オブジェクトを作成し、モジュール コード オブジェクトを実行します。
概念的には:```text
import module
find spec
create module object
compile or load code object
execute code object in module namespace
```したがって、評価ループはインポートに参加します。モジュールをインポートすると、コードが実行されます。
これが、インポート時の副作用が発生する理由です。```python
# module.py
print("imported")
import module
```モジュール本体の実行は通常のコード実行であるため、印刷が実行されます。
## 27.36 パフォーマンスモデル
評価ループは Python のパフォーマンスの多くを説明します。
Python の操作には、多くの場合、いくつかの層のコストがかかります。```text
bytecode dispatch
stack manipulation
reference count updates
dynamic type checks
dictionary lookup
descriptor protocol
function call overhead
allocation
error checks
```例えば:```python
obj.x + y
```以下が必要になる場合があります:```text
LOAD_FAST obj
LOAD_ATTR x
LOAD_FAST y
BINARY_OP +
```各命令にはインタープリタのオーバーヘッドがあります。`LOAD_ATTR`記述子の検索が含まれる場合があります。`BINARY_OP`数値ディスパッチが含まれる場合があります。参照カウントを維持する必要があります。エラーをチェックする必要があります。
これが、ホット ループを C 拡張機能、ベクトル化ライブラリ、または組み込み操作に移動する方がはるかに高速になる理由です。これらにより、評価ループによって実行されるバイトコード命令と動的ディスパッチの数が削減されます。
## 27.37 評価ループエスケープハッチとしての組み込み
組み込み操作は、バイトコード レベル以下で大量の作業を実行できます。
例:```python
sum(xs)
```評価ループは次の呼び出しを実行します。`sum`ただし、要素のループは組み込み実装内で C で実行できます。
比較する:```python
total = 0
for x in xs:
total += x
```これには、反復ごとに多くのバイトコード命令が必要です。
繰り返される作業の多くは C で行われるため、組み込みによりインタープリターのオーバーヘッドを削減できます。
これは Python の一般的なパフォーマンス原則です。```text
fewer Python bytecode instructions in hot paths usually means better performance
```## 27.38 Python からの評価ループの検査
バイトコードを勉強できます`dis`:
```python
import dis
def f(a, b):
c = a + b
return c
dis.dis(f)
```フレームを検査できます。```python
import inspect
def f():
frame = inspect.currentframe()
print(frame.f_code.co_name)
print(frame.f_locals)
f()
```呼び出しの深さを検査できます。```python
import sys
def f(n):
frame = sys._getframe()
print(n, frame.f_code.co_name)
if n:
f(n - 1)
f(3)
```実行を追跡できます。```python
import sys
def trace(frame, event, arg):
print(event, frame.f_code.co_name, frame.f_lineno)
return trace
def f(x):
y = x + 1
return y
sys.settrace(trace)
f(10)
sys.settrace(None)
```これらのツールは、評価ループが内部的に維持する機構の一部を公開します。
## 27.39 簡略化された評価ループ
ループの教育バージョンは次のようになります。```c
PyObject *
eval_frame(Frame *frame)
{
for (;;) {
Instruction instr = next_instruction(frame);
switch (instr.opcode) {
case OP_LOAD_CONST: {
PyObject *value = frame->code->consts[instr.arg];
push(frame, value);
break;
}
case OP_LOAD_FAST: {
PyObject *value = frame->locals[instr.arg];
if (value == NULL) {
raise_unbound_local_error();
goto error;
}
push(frame, value);
break;
}
case OP_STORE_FAST: {
PyObject *value = pop(frame);
frame->locals[instr.arg] = value;
break;
}
case OP_BINARY_ADD: {
PyObject *right = pop(frame);
PyObject *left = pop(frame);
PyObject *result = PyNumber_Add(left, right);
if (result == NULL) {
goto error;
}
push(frame, result);
break;
}
case OP_RETURN_VALUE: {
PyObject *result = pop(frame);
return result;
}
}
}
error:
return NULL;
}
```これでは、実際の詳細のほとんどが省略されています。```text
reference ownership
specialization
inline caches
exception tables
tracing
profiling
GIL checks
pending calls
signals
generators
coroutines
debug builds
statistics
opcode prediction
deoptimization
frame materialization
```しかし、それは本質的なアイデアを捉えています。
## 27.40 よくある誤解
|誤解 |正しいモデル |
|---|---|
| CPython はソース テキストを直接実行します。 CPython はコンパイルされたコード オブジェクトを実行します。
| Python 変数は生の値を保存します。名前とスロットはオブジェクトへの参照を保持します。
|バイトコードはバージョン間で安定しています。バイトコードは CPython 実装の詳細です |
|`a + b`簡単なマシン追加 |特殊化されていない限り、これは動的オブジェクト プロトコル ディスパッチです。
|フレームは単なるトレースバック オブジェクトです。フレームはアクティブな実行状態です |
| GIL はユーザー スレッドにのみ影響します。インタプリタの実行とオブジェクトの安全性と深く関係しています。
|例外はまれなサイド パスのみです。例外は通常の制御フロー機構に統合されます。
|ジェネレーターは特別な関数のみです。これらは再開可能な実行フレームまたは同等の状態です。
## 27.41 実際のソースを読む
実際の CPython ソースを読み取るときは、次の順序を使用します。
1. から始める`dis`小さな Python 関数の出力。
2. バイトコード命令を特定します。
3. 対応するオペコード定義を見つけます。
4. 生成された、または手書きのインタプリタ実装を見つけます。
5. オブジェクト操作のヘルパー呼び出しに従います。
6. 参照の所有権を追跡します。
7. スタック効果を追跡します。
8. エラー パスを追跡します。
9. 特殊化とキャッシュの動作を確認します。
10. Python のバージョン間の動作を比較します。
優れた学習機能は次のとおりです。```python
def example(obj, xs):
total = 0
for x in xs:
total += obj.value + x
return total
```この関数は多くのインタープリタ パスに影響します。```text
local variable access
loop iteration
attribute lookup
binary operation
in-place update semantics
jump instructions
return
```それを逆アセンブルし、各命令をインタプリタ機構にマッピングします。
## 27.42 章の概要
評価ループでは、コンパイルされた Python コードが Python の動作を実行します。フレームを通じてコード オブジェクトを実行し、スタック ベースのバイトコード モデルを使用し、命令をディスパッチし、参照を維持し、例外を処理し、関数を呼び出し、ランタイム イベントをチェックし、可能な場合は特殊化を適用します。
ループは概念としては小さいですが、結果としては大規模になります。これは、ほぼすべての CPython サブシステムのジャンクションに位置します。```text
compiler
frames
objects
types
reference counting
garbage collection
exceptions
calls
imports
generators
coroutines
tracing
profiling
threading
optimization
```CPython を理解するには、評価ループを理解する必要があります。機械の中の機械です。