クロージャの処理、具体的には外側の変数がどう扱われているのかを見ていきます。外側の変数というのは、
1 2 3 4 |
|
と書いたときのa、つまり、innerから見ると自分で定義してないのに使えている変数のことです。
では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のバイトコードを表示
>>> 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に入りましょう。
>>> 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が記録されています。このことから
と考えることができます。
ちなみに、引数で渡されてるけどクロージャでは使われていない変数はcellvarsには入らないし、コード中で定義されてないけどグローバル変数はfreevarsには入りません。それは以下のPythonプログラムを解析してみればわかります。
1 2 3 4 5 |
|
「外側の変数」とグローバル変数は明確に区別されているようです。 ここら辺の処理は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
それぞれ見てみる。
1
2
3
4
5
6
| - | | | | ! |
|
freevarsが何者なのか確認。念のためですが以下のコードはポインタ演算です。
1 |
|
localsplusはローカル変数+スタック領域なはずでした。さらにさかのぼってフレームを作るところ(PyFrame_New)を見てみる。
1
2
3
4
5
6
7
8
| - | | |
|
今の場合、ncellsは1でnfreesは0、nlocalsは2(aとinner)です。というわけでローカル変数領域の後ろにcellとfreeの領域がとられるようです。ncellsとnfreesが両方非0になるのは、関数内関数内関数を作るときぐらいか?(笑)
ところでcellの領域っていつ初期化されるの?ってところですが_PyEval_EvalCodeWithNameのいつも略している「引数処理」のところで行われています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | - ! - | | | | - | | | ! - | ! | | | ! |
|
先ほど、nlocalsは2でaが含まれていると言いましたが、cellvarに設定された場合はローカル変数領域の方は使わないようです。例えば、「a = a + 1」と書いた場合、STORE_FASTではなくSTORE_DEREFという命令が使われfreevars領域が更新されるようです。
コメントにあるようにすぐ下でfreevarsも設定していますがそちらはまた後で。
MAKE_FUNCTINで今までと違うのはopargが8になっている点です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| - | | | | | | | - | ! | - | | ! - ! | | ! |
|
見ての通り、積まれているタプルをfunc_closureという領域に格納しています。一応確認しておくとMAKE_FUNCTION実行前の時点でスタックは以下のようになっています。
'outer.<locals>.inner' innerのコードオブジェクト (PyCell(a),)
というわけでMAKE_FUNCTION実行時点のaの値が保存されたということになります。
ここまでわかれば参照側はわかったようなものですが
4 0 LOAD_DEREF 0 (a)
LOAD_DEREFの処理部分
先ほど後で見ると言ったfreevarsの設定を確認しましょう。
1
2
3
4
5
6
| - | | | ! |
|
closureは先ほど保存したタプルです。呼び出し部分を見ていませんがFuncObjectからfunc_closureを取り出し_PyEval_EvalCodeWithNameに渡しています(closure引数に設定している)
今回はクロージャの処理について見てきました。Rubyだとこのあたりの処理がなかなか複雑で外側の変数が参照されるとフレームをさかのぼってた気がしますがPythonはかなりシンプルですね。 クロージャを作る瞬間の変数情報(変数が指しているオブジェクト)をタプルに保存してしまい、クロージャを使う側はそのタプルから値を取り出して関数実行前にもう設定してしまう。これによりコードの実行は素早く行えることになります。いやまあそもそも遅いですけど(笑)