Pythonを読む
_PyEval_EvalCodeWithName (Python/ceval.c) †
バイトコードができたのでいよいよ実行です。バイトコードの実行のエントリーポイントはceval.cに書かれたPyEval_EvalCode、PyEval_EvalCodeEx、_PyEval_EvalCodeWithNameと呼ばれて行き、これがメインの処理を行っているようです。
さて、_PyEval_EvalCodeWithNameですが大部分は引数の処理を行っています。今回は引数はないのでサクッと無視すると、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
-
|
|
|
|
|
|
|
-
!
|
|
|
|
-
|
|
|
!
|
|
|
|
!
| static PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
PyObject **args, int argcount, PyObject **kws, int kwcount,
PyObject **defs, int defcount, PyObject *kwdefs, PyObject *closure,
PyObject *name, PyObject *qualname)
{
PyCodeObject* co = (PyCodeObject*)_co;
PyFrameObject *f;
PyObject *retval = NULL;
PyThreadState *tstate = PyThreadState_GET();
f = PyFrame_New(tstate, co, globals, locals);
retval = PyEval_EvalFrameEx(f,0);
fail:
++tstate->recursion_depth;
Py_DECREF(f);
--tstate->recursion_depth;
return retval;
}
|
実行オブジェクトに対してフレームを割り当て、実行しています。
PyFrame_New (Objects/frameobject.c) †
まずフレーム構造について確認します。PyFrameObjectの定義はInclude/frameobject.hにあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| -
|
|
|
|
|
|
|
-
|
!
|
|
|
-
|
|
|
|
|
!
|
|
|
|
|
|
|
|
|
|
!
| typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back;
PyCodeObject *f_code;
PyObject *f_builtins;
PyObject *f_globals;
PyObject *f_locals;
PyObject **f_valuestack;
PyObject **f_stacktop;
PyObject *f_trace;
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
PyObject *f_gen;
int f_lasti;
int f_lineno;
int f_iblock;
char f_executing;
PyTryBlock f_blockstack[CO_MAXBLOCKS];
PyObject *f_localsplus[1];
} PyFrameObject;
|
普通のフレーム構造です。少し特殊なのはf_localsplusですね。コメントによるとローカル変数とスタックに使われるようです。
ではPyFame_Newを見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
-
|
|
|
|
|
-
!
-
|
|
|
!
-
|
|
|
|
-
|
!
-
|
|
|
-
|
|
!
|
!
|
|
|
|
|
|
|
|
|
!
|
|
|
|
|
|
|
|
|
|
-
|
|
!
-
|
|
|
|
!
|
|
|
|
|
|
|
|
|
!
| PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
PyObject *locals)
{
PyFrameObject *back = tstate->frame;
PyFrameObject *f;
PyObject *builtins;
Py_ssize_t i;
if (code->co_zombieframe != NULL) {
f = code->co_zombieframe;
code->co_zombieframe = NULL;
_Py_NewReference((PyObject *)f);
}
else {
Py_ssize_t extras, ncells, nfrees;
ncells = PyTuple_GET_SIZE(code->co_cellvars);
nfrees = PyTuple_GET_SIZE(code->co_freevars);
extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
if (free_list == NULL) {
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras);
}
else {
--numfree;
f = free_list;
free_list = free_list->f_back;
if (Py_SIZE(f) < extras) {
PyFrameObject *new_f = PyObject_GC_Resize(PyFrameObject, f, extras);
f = new_f;
}
_Py_NewReference((PyObject *)f);
}
f->f_code = code;
extras = code->co_nlocals + ncells + nfrees;
f->f_valuestack = f->f_localsplus + extras;
for (i=0; if_localsplus[i] = NULL;
f->f_locals = NULL;
f->f_trace = NULL;
f->f_exc_type = f->f_exc_value = f->f_exc_traceback = NULL;
}
f->f_stacktop = f->f_valuestack;
f->f_builtins = builtins;
Py_XINCREF(back);
f->f_back = back;
Py_INCREF(code);
Py_INCREF(globals);
f->f_globals = globals;
if ((code->co_flags & (CO_NEWLOCALS | CO_OPTIMIZED)) == (CO_NEWLOCALS | CO_OPTIMIZED))
;
else if (code->co_flags & CO_NEWLOCALS) {
locals = PyDict_New();
f->f_locals = locals;
}
else {
if (locals == NULL)
locals = globals;
Py_INCREF(locals);
f->f_locals = locals;
}
f->f_lasti = -1;
f->f_lineno = code->co_firstlineno;
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;
_PyObject_GC_TRACK(f);
return f;
}
|
エラー処理とかはさっくり省いて、効率アップのためにフレームの生成が分岐しています。
- 前にそのCodeObjectを実行したフレーム(zombieframe)があればそれを使う
- free_listにフレームがあればそれを使う(スタックが必要とされる数以下なら確保を行う)
- zombieもfree_listもなければ生成を行う
PyObject_GC_NewVarはややこしいのですが、これはInclude/objimpl.hにあるマクロで、Modules/gcmodule.cにある_PyObject_GC_NewVarが関数の本体です。
1
2
3
4
5
6
7
8
9
10
11
12
|
-
|
|
|
|
|
|
|
|
!
| PyVarObject *
_PyObject_GC_NewVar(PyTypeObject *tp, Py_ssize_t nitems)
{
size_t size;
PyVarObject *op;
size = _PyObject_VAR_SIZE(tp, nitems);
op = (PyVarObject *) _PyObject_GC_Malloc(size);
if (op != NULL)
op = PyObject_INIT_VAR(op, tp, nitems);
return op;
}
|
_PyObject_VAR_SIZEはInclude/objimpl.hにあります。
1
2
3
4
|
| #define _PyObject_VAR_SIZE(typeobj, nitems) \
_Py_SIZE_ROUND_UP((typeobj)->tp_basicsize + \
(nitems)*(typeobj)->tp_itemsize, \
SIZEOF_VOID_P)
|
TypeObjectについては詳しく見ませんがPyFrame_Typeの場合以下のようになっています。(Objects/frameobject.cに書かれています)
1
2
3
4
5
6
7
| -
|
|
|
|
-
!
| PyTypeObject PyFrame_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"frame",
sizeof(PyFrameObject),
sizeof(PyObject *),
};
|
sizeof(PyFrameObject)がtp_basicsize、sizeof(PyObject *)がtp_itemsizeです。つまり、PyFrameObjectの後にPyObject*の配列の領域が確保されることになり、PyFrameObjectの最終メンバーf_localsplusで参照できるということになります。
PyEval_EvalFrameEx (Python/ceval.c) †
フレームについて確認できたのでいよいよコードの実行です。
さて、実行方法ですが、よくあるダイレクトスレッデッドコードです。ダイレクトスレッデッドコードって何?という人向けに大まかに説明すると、
- 各命令に対応するswitchのcaseを用意し、ループをひたすら回す
- でもそれだとループする分遅くなってしまうので、次の命令に直接gotoする(命令と実行アドレスの対応表を作っておく)
というものです。PyEval_EvalFrameExの上の方にその説明および実装(マクロ)が書かれています。
後、ここまで見てきて想像はつくと思いますがPythonのVMはスタックマシンのようです。これらがわかると、後は各命令コードについて見ていくだけです。というわけでいつものようにコードを実行してみます。
LOAD_CONST †
LOAD_CONSTは定数をスタックにプッシュしています。ここでいう定数というのは数値や文字列以外に実行コードも含まれるようです。コード生成の際に内包表記のコードをLOAD_CONSTしてましたね。
LOAD_FAST †
LOAD_FASTはローカル変数をスタックにプッシュしています。ただしこちらは引数がオブジェクト本体ではなくローカル変数領域の何番目に入っているかのインデックスになります。
LOAD_NAME †
名前をローカル変数→グローバル変数→ビルトインの順番で検索し、見つかったものをスタックにプッシュしています。今回の場合、rangeで使われていますが、これはビルトインです。
STORE_FAST †
STORE_FASTはLOAD_FASTの逆、スタックのトップをローカル変数領域に保存しています。
BINARY_MULTIPLY †
スタックの一つ目と二つ目を取ってきて掛け算して結果をスタックに戻します。普通のスタックマシンでの掛け算処理です。
COMPARE_OP †
スタックの一つ目と二つ目の比較処理。どのような比較を行うかは引数になっててcmp_outcome関数で実際の比較処理を行うようです。cmp_outcomeは簡単な比較処理だけしてより詳細な比較が必要な場合はさらにPyObject_RichCompareへ。Rubyの場合はプリミティブは特別処理していた気がしますがPythonはnumpyとかあるし再定義前提で個別の処理はしていないのかな?よく思うとMULTIPLYもか。
BUILD_LIST †
リストを作成しスタックにプッシュしています。
1
2
3
4
5
6
7
8
9
10
11
| -
|
|
|
-
|
|
!
|
|
!
| TARGET(BUILD_LIST) {
PyObject *list = PyList_New(oparg);
if (list == NULL)
goto error;
while (--oparg >= 0) {
PyObject *item = POP();
PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();
}
|
引数で指定された分スタックからポップし詰めていますが今回は引数の値は0なので空リストです。後ろから詰めているようなのでスタックには最終要素が一番上になるように置くようですね。
LIST_APPEND †
引数で指定した位置にあるリストにスタックトップの要素を追加しています。
1
2
3
4
5
6
7
8
9
10
11
| -
|
|
|
|
|
|
|
|
|
!
| TARGET(LIST_APPEND) {
PyObject *v = POP();
PyObject *list = PEEK(oparg);
int err;
err = PyList_Append(list, v);
Py_DECREF(v);
if (err != 0)
goto error;
PREDICT(JUMP_ABSOLUTE);
DISPATCH();
}
|
コード生成の際にLIST_APPENDでは2が指定されていました。つまり、一番上にある要素をその下にあるリストに追加していたという意味だったようです。
POP_JUMP_IF_FALSE †
スタックトップをポップしてFalseだったら指定されたラベル(にある命令コード)に飛ぶ、普通の分岐命令です。
JUMP_ABSOLUTE †
説明する必要もないような絶対ジャンプです。
MAKE_FUNCTION †
クロージャの処理とかしていますが今回は関係ないのでさっくり無視すると、
1
2
3
4
5
6
7
8
9
10
11
|
-
|
|
|
|
|
|
|
|
!
| TARGET(MAKE_FUNCTION)
_make_function: {
PyObject *qualname = POP();
PyObject *code = POP();
PyObject *func = PyFunction_NewWithQualName(code, f->f_globals, qualname);
Py_DECREF(code);
Py_DECREF(qualname);
PUSH(func);
DISPATCH();
}
|
使われている箇所は以下のような感じ。関数を作り、それをスタックに積むということをしているようです。
LOAD_CONST 内包表記のコード
LOAD_CONST <listcomp>
MAKE_FUNCTION 0
CALL_FUNCTION †
関数呼び出しです。(そのまんま)
1
2
3
4
5
6
7
8
9
10
| -
|
|
|
|
|
|
|
|
!
| TARGET(CALL_FUNCTION) {
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg);
stack_pointer = sp;
PUSH(res);
if (res == NULL)
goto error;
DISPATCH();
}
|
call_functionではCで実装された関数なのかPythonで書かれた関数なのか、引数の数や種類などによって関数の呼び出し方法が分かれています。
なお、呼び出す関数や関数の引数はスタックに積まれています。rangeを呼んでいる部分は以下のようになっていましたね。
LOAD_NAME 0(range)
LOAD_CONST 10
CALL_FUNCTION 1
念のため確認すると、
10
range
というように引数が上、呼び出す関数が下に積まれています。
rangeの場合、関数のように見えますが実体はTypeObjectなので、do_call→PyObject_Callと呼び出しが進みます。
PyObject_CallはObjects/abstract.cにあります。TypeObjectのtp_callに登録されているものが呼び出されます。
さて、rangeの実装はObjects/rangeobject.cにあります。その中にPyRange_Typeの定義がありますがtp_callはNULLです。tp_callがNULLだとPyObject_Callはエラーになるのですが実際にはエラーにはなりません。そのからくりは、rangeがtypeのサブクラスのためです。
ちょっと横道にそれますが、ここら辺の処理は初めに深く追うのをやめたPy_Initialize以下で行われていました。_Py_InitializeEx_Private(Python/pylifecycle.c)→_Py_ReadyTypes(Objects/object.c)→PyType_Ready(object/typeobject.c)→inherit_slotsと進んでいき、サブクラスでメソッドが定義されていないのでスーパークラスのものが参照されるようになります。
というわけで、type_callが呼び出されるわけですが、ここでtp_newに指定された関数を呼び出しています。tp_newにはrange_newが指定されているので結果、range_newが呼び出されます。
GET_ITER †
というわけでスタックにrangeオブジェクトが積まれました。生成されたコードではその後、GET_ITERを呼び出しています。
1
2
3
4
5
6
7
8
9
10
11
| -
|
|
|
|
|
|
|
|
|
!
| TARGET(GET_ITER) {
PyObject *iterable = TOP();
PyObject *iter = PyObject_GetIter(iterable);
Py_DECREF(iterable);
SET_TOP(iter);
if (iter == NULL)
goto error;
PREDICT(FOR_ITER);
DISPATCH();
}
|
rangeオブジェクトがrangeオブジェクトのイテレータオブジェクトに置き換わりました。
CALL_FUNCTION(Pythonでの実装の場合) †
GET_ITER後、再度CALL_FUNCTIONが実行されています。
GET_ITER
CALL_FUNCTION 1
スタックトップにはイテレータオブジェクト、その下には内包表記のコードが置かれています。というわけで、イテレータオブジェクトを引数に内包表記のコードが呼び出されます。
さて、内包表記のコードはPythonで書かれているので先ほどとは呼び出し方法が違います。具体的には、call_functionからfast_functionに処理が進みます。処理が分岐はしていますがいずれにしろ新しいフレームが作成され、関数コードの実行が行われます。
FOR_ITER †
内包表記のコードではFOR_ITERがポイントになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| -
|
|
|
-
|
|
|
|
!
-
|
|
|
|
|
!
|
|
|
|
|
!
| TARGET(FOR_ITER) {
PyObject *iter = TOP();
PyObject *next = (*iter->ob_type->tp_iternext)(iter);
if (next != NULL) {
PUSH(next);
PREDICT(STORE_FAST);
PREDICT(UNPACK_SEQUENCE);
DISPATCH();
}
if (PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_StopIteration))
goto error;
else if (tstate->c_tracefunc != NULL)
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
PyErr_Clear();
}
STACKADJ(-1);
Py_DECREF(iter);
JUMPBY(oparg);
DISPATCH();
}
|
FOR_ITERの使われてるところは以下のようになっています。
BUILD_LIST 0
LOAD_FAST 0
:start
FOR_ITER REL:anchor
:
STORE_FAST 0(n)
LOAD_FAST 0(n)
LOAD_FAST 0(n)
BINARY_MULTIPLY
LOAD_CONST 10
COMPARE_OP >
POP_JUMP_IF_FALSE ABS:if_cleanup
:
LOAD_FAST 0(n)
LIST_APPEND 2
:skip
:if_cleanup
JUMP_ABSOLUTE ABS:start
:anchor
RETURN_VALUE
LOAD_FASTで渡された引数(イテレータオブジェクト)をスタックに積み、それから要素を取得して処理、要素がもうなくなったら引数で指定されたところまでジャンプするということを行っています。結果、繰り返しが終わるとRETURN_VALUEが実行されることになります。
RETURN_VALUE †
スタックトップを戻り値とし、PyEval_EvalFrameExを終了します。今回の場合で言うと、スタックトップに残っているのは内包表記で作成したリストですね。これでトップレベルに戻り、作成されたリストが改めてスタックに積まれ、最後にPRINT_EXPRが呼び出されることで画面への出力が行われます。
おわりに †
というわけで、シェルで
1
|
| [n for n in range(10) if n * n > 10]
|
と打った時にどのようなことが行われるかを見てきました。Rubyと似ているところもあれば違うところもありました。ツリー作成が二段階なところ、シンボルテーブルを作るタイミング、呼び出す関数の指定、関数の実行方法などはRubyと異なっていたように思います。
それではみなさんもよいコードリーディングを。