この記事は人間でもわかるPythonバイトコードシリーズ第2回目の記事ですが、この記事から見始めても大丈夫です。
前回の記事はこちらです。
前回の記事で、次は.pycファイルのフォーマットについて説明すると書きました。が、Pythonバイトコードの仕様について調べているうちにクラスと関数の__興味深い関係__を発見しましたので、予定を変更し、タイトルも独立したものとして記事を書くこととしました。
環境
CPython 3.9.0
Pythonは関数をどう定義しているか?
まずPythonインタープリタ内での関数の扱われ方について見ていきましょう。
次のスクリプトのバイトコードを逆アセンブルします。逆アセンブルとはなんぞという人は前回の記事を参照してください。
def f(x, y):
return x + y
2つの引数を足す関数を定義しただけのスクリプトです。結果は以下のようになります。
1 0 LOAD_CONST 0 (<code object f at 0x10ddb4190, file "test1.py", line 1>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object f at 0x10ddb4190, file "test1.py", line 1>:
2 0 LOAD_FAST 0 (x)
2 LOAD_FAST 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
Pythonは関数を第一級オブジェクトととして扱います。つまり、関数を宣言できるだけは足りず、オブジェクトとして動的に生成する命令が必要になります。それこそが4バイト目にあるMAKE_FUNCTION
です。
関数の生成に必要なものは、中身を表すコードオブジェクトと、関数の名前です。そのためMAKE_FUNCTION
の前に、LOAD_CONST
でコードオブジェクトと関数の名前fをロードしています。しかしコードオブジェクトには既にfという名前がついています。代入命令であるSTORE_NAME
と合わせると、3回も関数の名前fが登場しています。くどいですね。
これはPythonバイトコードに無名関数を生成する命令がないからとも言えます。Pythonの無名関数(lambda式)は後付けの機能のため、単に<lambda>
という名前の関数を生成することによって実現されています。
lambda x: x + 1
↓
1 0 LOAD_CONST 0 (<code object <lambda> at 0x10c470190, file "test1.py", line 1>)
2 LOAD_CONST 1 ('<lambda>')
4 MAKE_FUNCTION 0
6 POP_TOP
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object <lambda> at 0x10c470190, file "test1.py", line 1>:
1 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 RETURN_VALUE
コードオブジェクトとは?
気になるのはコードオブジェクトが一体何モノであるかということです。コードオブジェクトについては前回少し説明しましたが、その本質は__スコープの表現__です。コードオブジェクトはスコープを実現するために変数の名前テーブルを持っています。
よってPythonでスコープを持つものはなんでもコードオブジェクトを持っています(ex. 関数、クラス、モジュール)。逆にコードオブジェクトを使って表現されないものはスコープを持ちません。その例がifやwhile、for文などです。
for i in range(1):
j = 1
print(j)
このスクリプトを実行すると1が出力されますが、同じことをC言語でやろうとするとエラーになります。
このようなキモコードが動くカラクリは、まさにバイトコードを覗くことによって理解できるのです。
1 0 LOAD_NAME 0 (range)
2 LOAD_CONST 0 (1)
4 CALL_FUNCTION 1
6 GET_ITER
>> 8 FOR_ITER 8 (to 18)
10 STORE_NAME 1 (i)
2 12 LOAD_CONST 0 (1)
14 STORE_NAME 2 (j)
16 JUMP_ABSOLUTE 8
3 >> 18 LOAD_NAME 3 (print)
20 LOAD_NAME 2 (j)
22 CALL_FUNCTION 1
24 POP_TOP
26 LOAD_CONST 1 (None)
28 RETURN_VALUE
16バイト目のJUMP_ABSOLUTE
から分かるように、for文は原始的なジャンプ機能を使って実現されています。10~16バイト目が内側の処理に相当し、FOR_ITER
にてイテレータが使い切られるまでループが続くというわけです。そしてjの定義が参照と同じフレーム内で行われているため、エラーにならないのです。
関数の例からすると、for文が内側の処理をコードオブジェクトを使って閉じ込めても良さそうなものです。しかし実際にはそうなっていません。これは恐らく高速化のためでしょう。ジャンプのように処理が低級であるほど実行は高速になります(それでもPythonのforは素晴らしく遅いですが)。あとループ変数の初期化をしなくて済むというメリットもありますね。
クラスと関数
これでクラスと関数の関係性について説明できます。
先ほど言った通り、Pythonでは関数とクラスは独自のスコープを持ちます。
そのスコープはコードオブジェクトを用いて表されるので、この点で関数とクラスが共通点を持つことがわかります。
では、次のスクリプトはどのようになるでしょうか?
def f():
x = 1
class F:
x = 1
print(f.x, F.x)
上のスクリプトのバイトコードは以下のように逆アセンブルされます(注:実行するとエラーになります)。
1 0 LOAD_CONST 0 (<code object f at 0x101033190, file "test1.py", line 1>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
4 8 LOAD_BUILD_CLASS
10 LOAD_CONST 2 (<code object F at 0x101033240, file "test1.py", line 4>)
12 LOAD_CONST 3 ('F')
14 MAKE_FUNCTION 0
16 LOAD_CONST 3 ('F')
18 CALL_FUNCTION 2
20 STORE_NAME 1 (F)
7 22 LOAD_NAME 2 (print)
24 LOAD_NAME 0 (f)
26 LOAD_ATTR 3 (x)
28 LOAD_NAME 1 (F)
30 LOAD_ATTR 3 (x)
32 CALL_FUNCTION 2
34 POP_TOP
36 LOAD_CONST 4 (None)
38 RETURN_VALUE
Disassembly of <code object f at 0x101033190, file "test1.py", line 1>:
2 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (x)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
Disassembly of <code object F at 0x101033240, file "test1.py", line 4>:
4 0 LOAD_NAME 0 (__name__)
2 STORE_NAME 1 (__module__)
4 LOAD_CONST 0 ('F')
6 STORE_NAME 2 (__qualname__)
5 8 LOAD_CONST 1 (1)
10 STORE_NAME 3 (x)
12 LOAD_CONST 2 (None)
14 RETURN_VALUE
0~6バイト目が関数f
の生成、8~20バイト目がクラスF
の生成です。
なんだか似たような行程を踏んでいると分かります。特にFの生成では、クラスの生成なのになぜかMAKE_FUNCTION
が呼ばれているではありませんか。
8バイト目のLOAD_BUILD_CLASS
がポイントです。この命令は特殊な組み込み関数である__build_class__
をロードします。この関数は18バイト目で呼び出されています。
この関数はメタクラスを生成する関数です。
メタクラスはクラスのクラスです。つまり、メタクラスのインスタンスはクラスになります。関数が第一級オブジェクトなのだから当然クラスも第一級オブジェクトとして扱いたいわけで、この関数が必要になります。
__build_class__
のCPythonでの実装を見てみましょう。
static PyObject *
builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
PyObject *kwnames)
{
PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *orig_bases;
PyObject *cls = NULL, *cell = NULL;
int isclass = 0; /* initialize to prevent gcc warning */
if (nargs < 2) {
PyErr_SetString(PyExc_TypeError,
"__build_class__: not enough arguments");
return NULL;
}
func = args[0]; /* Better be callable */
if (!PyFunction_Check(func)) {
PyErr_SetString(PyExc_TypeError,
"__build_class__: func must be a function");
return NULL;
}
...
cell = PyEval_EvalCodeEx(PyFunction_GET_CODE(func), PyFunction_GET_GLOBALS(func), ns,
NULL, 0, NULL, 0, NULL, 0, NULL,
PyFunction_GET_CLOSURE(func));
...
これを見るに、この関数の第一引数は関数オブジェクトであることがわかります。その関数をもとにcellというオブジェクトを作ります。そしてこの後、cellからクラスが生成されます。
......これでこの記事のタイトルの真意が理解できたと思います。
なんと、__クラスのボディは関数を流用していた__のです!
終わりに
今回は以上です。ここまで読んでいただきありがとうございました。
次回は当初の予定通り.pycファイルのフォーマットについて解説します。