Pythonを読む

はじめに

クロージャの処理、具体的には外側の変数がどう扱われているのかを見ていきます。外側の変数というのは、

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
def outer(a):
    def inner(b):
        return a + b
    return inner

と書いたときのa、つまり、innerから見ると自分で定義してないのに使えている変数のことです。

compile, disを使っての確認

ではcompile関数とdisモジュールを使ってコードオブジェクトとバイトコードを確認しましょう。

>>> src = '''
def outer(a):
    def inner(b):
        return a + b
    return inner
'''
>>> co = compile(src, '<string>', 'exec')
>>> co.co_consts
(<code object outer at 0x0250C3E8, file "<string>", line 2>, 'outer', None)

outer関数

outerのバイトコードを表示

>>> dis.dis(co.co_consts[0])
  3           0 LOAD_CLOSURE             0 (a)
              2 BUILD_TUPLE              1
              4 LOAD_CONST               1 (<code object inner at 0x0250C390, file "<string>", line 3>)
              6 LOAD_CONST               2 ('outer.<locals>.inner')
              8 MAKE_FUNCTION            8
             10 STORE_FAST               1 (inner)

  5          12 LOAD_FAST                1 (inner)
             14 RETURN_VALUE

さっそくクロージャという単語が出てきました。

innerに進む前にouterのコードオブジェクトについてもう少し見てみましょう。コードオブジェクトにはco_cellvarsとco_freevarsという属性があります。

>>> co.co_consts[0].co_cellvars
('a',)
>>> co.co_consts[0].co_freevars
()

cellvarsにaが記録されています。

inner関数

では、innerに入りましょう。

>>> dis.dis(co.co_consts[0].co_consts[1])
  4           0 LOAD_DEREF               0 (a)
              2 LOAD_FAST                0 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

LOAD_DEREFという見慣れない命令が使われています。

co_cellvarsとco_freevarsを確認します。

>>> co.co_consts[0].co_consts[1].co_cellvars
()
>>> co.co_consts[0].co_consts[1].co_freevars
('a',)

今度はfreevarsにaが記録されています。このことから

  • freevarとはコード内で使ってるんだけどコード中で定義されていない変数
  • cellvarとはクロージャで使われてる変数

と考えることができます。

ちなみに、引数で渡されてるけどクロージャでは使われていない変数はcellvarsには入らないし、コード中で定義されてないけどグローバル変数はfreevarsには入りません。それは以下のPythonプログラムを解析してみればわかります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
 
 
 
c = 3
def outer(a, d):
    def inner(b):
        return a + b + c
    return inner

「外側の変数」とグローバル変数は明確に区別されているようです。 ここら辺の処理はAST作った後のシンボルテーブル作成で行われているのですが、長いので割愛します。

クロージャ作る側の処理

では本編に入りましょう。クロージャを作る側で関係ありそうな部分は以下の通りです。

  3           0 LOAD_CLOSURE             0 (a)
              2 BUILD_TUPLE              1
              4 LOAD_CONST               1 (<code object inner at 0x0250C390, file "<string>", line 3>)
              6 LOAD_CONST               2 ('outer.<locals>.inner')
              8 MAKE_FUNCTION            8

それぞれ見てみる。

LOAD_CLOSURE周りの処理

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
-
|
|
|
|
!
        TARGET(LOAD_CLOSURE) {
            PyObject *cell = freevars[oparg];
            Py_INCREF(cell);
            PUSH(cell);
            DISPATCH();
        }

freevarsが何者なのか確認。念のためですが以下のコードはポインタ演算です。

Everything is expanded.Everything is shortened.
  1
 
    freevars = f->f_localsplus + co->co_nlocals;

localsplusはローカル変数+スタック領域なはずでした。さらにさかのぼってフレームを作るところ(PyFrame_New)を見てみる。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
 
-
|
|
        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);

今の場合、ncellsは1でnfreesは0、nlocalsは2(aとinner)です。というわけでローカル変数領域の後ろにcellとfreeの領域がとられるようです。ncellsとnfreesが両方非0になるのは、関数内関数内関数を作るときぐらいか?(笑)

ところでcellの領域っていつ初期化されるの?ってところですが_PyEval_EvalCodeWithNameのいつも略している「引数処理」のところで行われています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
!
-
|
|
|
|
-
|
|
|
!
-
|
!
|
|
|
!
    /* Allocate and initialize storage for cell vars, and copy free
       vars into frame. */
    for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {
        PyObject *c;
        int arg;
        /* Possibly account for the cell variable being an argument. */
        if (co->co_cell2arg != NULL &&
            (arg = co->co_cell2arg[i]) != CO_CELL_NOT_AN_ARG) {
            c = PyCell_New(GETLOCAL(arg));
            /* Clear the local copy. */
            SETLOCAL(arg, NULL);
        }
        else {
            c = PyCell_New(NULL);
        }
        if (c == NULL)
            goto fail;
        SETLOCAL(co->co_nlocals + i, c);
    }

先ほど、nlocalsは2でaが含まれていると言いましたが、cellvarに設定された場合はローカル変数領域の方は使わないようです。例えば、「a = a + 1」と書いた場合、STORE_FASTではなくSTORE_DEREFという命令が使われfreevars領域が更新されるようです。

コメントにあるようにすぐ下でfreevarsも設定していますがそちらはまた後で。

MAKE_FUNCTIONでの処理

MAKE_FUNCTIONで今までと違うのはopargが8になっている点です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
-
|
|
|
|
|
|
|
-
|
!
|
-
|
|
!
-
!
|
|
!
        TARGET(MAKE_FUNCTION) {
            PyObject *qualname = POP();
            PyObject *codeobj = POP();
            PyFunctionObject *func = (PyFunctionObject *)
                PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);
 
            Py_DECREF(codeobj);
            Py_DECREF(qualname);
            if (func == NULL) {
                goto error;
            }
 
            if (oparg & 0x08) {
                assert(PyTuple_CheckExact(TOP()));
                func ->func_closure = POP();
            }
            // 省略
 
            PUSH((PyObject *)func);
            DISPATCH();
        }

見ての通り、積まれているタプルをfunc_closureという領域に格納しています。一応確認しておくとMAKE_FUNCTION実行前の時点でスタックは以下のようになっています。

'outer.<locals>.inner'
innerのコードオブジェクト
(PyCell(a),)

というわけでMAKE_FUNCTION実行時点のaの値が保存されたということになります。

クロージャ側の処理

ここまでわかれば参照側はわかったようなものですが

  4           0 LOAD_DEREF               0 (a)

LOAD_DEREFの処理部分

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
-
|
|
-
|
|
!
|
|
|
!
        TARGET(LOAD_DEREF) {
            PyObject *cell = freevars[oparg];
            PyObject *value = PyCell_GET(cell);
            if (value == NULL) {
                format_exc_unbound(co, oparg);
                goto error;
            }
            Py_INCREF(value);
            PUSH(value);
            DISPATCH();
        }

先ほど後で見ると言ったfreevarsの設定を確認しましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
-
|
|
|
!
    /* Copy closure variables to free variables */
    for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) {
        PyObject *o = PyTuple_GET_ITEM(closure, i);
        Py_INCREF(o);
        freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;
    }

closureは先ほど保存したタプルです。呼び出し部分を見ていませんがFuncObjectからfunc_closureを取り出し_PyEval_EvalCodeWithNameに渡しています(closure引数に設定している)

おわりに

今回はクロージャの処理について見てきました。Rubyだとこのあたりの処理がなかなか複雑で外側の変数が参照されるとフレームをさかのぼってた気がしますがPythonはかなりシンプルですね。 クロージャを作る瞬間の変数情報(変数が指しているオブジェクト)をタプルに保存してしまい、クロージャを使う側はそのタプルから値を取り出して関数実行前にもう設定してしまう。これによりコードの実行は素早く行えることになります。いやまあそもそも遅いですけど(笑)


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-01-02 (火) 09:46:19 (2307d)