- 追加された行はこの色です。
- 削除された行はこの色です。
[[Pythonを読む]]
#contents
*はじめに [#k3f522bc]
クロージャの処理、具体的には外側の変数がどう扱われているのかを見ていきます。外側の変数というのは、
#code(Python){{
def outer(a):
def inner(b):
return a + b
return inner
}}
と書いたときのa、つまり、innerから見ると自分で定義してないのに使えている変数のことです。
*compile, disを使っての確認 [#ud8cea74]
では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関数 [#m865cc96]
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関数 [#ba8f2c14]
では、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プログラムを解析してみればわかります。
#code(Python){{
c = 3
def outer(a, d):
def inner(b):
return a + b + c
return inner
}}
「外側の変数」とグローバル変数は明確に区別されているようです。
ここら辺の処理はAST作った後のシンボルテーブル作成で行われているのですが、長いので割愛します。
*クロージャ作る側の処理 [#h233728e]
では本編に入りましょう。クロージャを作る側で関係ありそうな部分は以下の通りです。
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周りの処理 [#kdc33e80]
#code(C){{
TARGET(LOAD_CLOSURE) {
PyObject *cell = freevars[oparg];
Py_INCREF(cell);
PUSH(cell);
DISPATCH();
}
}}
freevarsが何者なのか確認。念のためですが以下のコードはポインタ演算です。
#code(C){{
freevars = f->f_localsplus + co->co_nlocals;
}}
localsplusはローカル変数+スタック領域なはずでした。さらにさかのぼってフレームを作るところ(PyFrame_New)を見てみる。
#code(C){{
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のいつも略している「引数処理」のところで行われています。
#code(C){{
/* 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での処理 [#r4742ae2]
MAKE_FUNCTINで今までと違うのはopargが8になっている点です。
#code(C){{
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の値が保存されたということになります。
*クロージャ側の処理 [#l9e8b05e]
ここまでわかれば参照側はわかったようなものですが
4 0 LOAD_DEREF 0 (a)
LOAD_DEREFの処理部分
#code(C){{
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の設定を確認しましょう。
#code(C){{
/* 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引数に設定している)
*おわりに [#a47e2bd0]
今回はクロージャの処理について見てきました。Rubyだとこのあたりの処理がなかなか複雑で外側の変数が参照されるとフレームをさかのぼってた気がしますがPythonはかなりシンプルですね。
クロージャを作る瞬間の変数情報(変数が指しているオブジェクト)をタプルに保存してしまい、クロージャを使う側はそのタプルから値を取り出して関数実行前にもう設定してしまう。これによりコードの実行は素早く行えることになります。いやまあそもそも遅いですけど(笑)