5
5

More than 3 years have passed since last update.

Pythonのクラスの中身は関数である

Last updated at Posted at 2021-02-12

この記事は人間でもわかる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バイト目で呼び出C A L Lされています。

この関数はメタクラスを生成する関数です。
メタクラスはクラスのクラスです。つまり、メタクラスのインスタンスはクラスになります。関数が第一級オブジェクトなのだから当然クラスも第一級オブジェクトとして扱いたいわけで、この関数が必要になります。
__build_class__のCPythonでの実装を見てみましょう。

cpython/Python/bltinmodule.c
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ファイルのフォーマットについて解説します。

参考

Python/ビルトインがビルトインされるまで - Code Reading Wiki

メタクラス - Wikipedia

5
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5