このBlogは移転しました。今後は aish.dev を御覧ください。

クロージャのひみつ

やっとPythonのクロージャの仕組みを少しは理解した件 で、清水川先生がクロージャを返すとき実行中のフレームオブジェクトが保存されるのか?という疑問を呈されている。

結論から言うと、フレームオブジェクトは保存されず、クロージャが必要とするオブジェクトだけが残される。この辺の仕組みがを簡単に解説してみよう。尚、以下の解説はPython2のものであり、Python3については未調査である。

例として、こんな関数を考えてみよう。

def func():
    a = 100
    b = 200
    c = 300
    
    def func2():
        return a, b
    
    a = 400
    
    return func2

この例では、func2 は func のローカル変数 a と b を参照するクロージャである。実行すると、(400, 200) を返す。


さて、関数を実行中、そのローカル変数は "セル"(cell)というオブジェクトの配列に格納されている。 func ではこんな感じになる。

セルオブジェクトは特別な機能は持たず、ただ他のオブジェクトへの参照を保持するだけのオブジェクトだ。 a = 400 のようにローカル変数の変更が発生すると、上図で a の値を保持するセルオブジェクトが参照するオブジェクトが変更される。

この関数で、クロージャ func2 を生成するとき、func2 で使用するローカル変数(ここでは a と b)のセルオブジェクトのタプルを作成し、fucn2 に設定する。

そして func2 を実行するとき、渡されたセルオブジェクトを使用してローカル変数の配列を生成し、その値を参照するのだ。

func2 に渡されるのは、a が参照しているオブジェクトではなく、 aの参照先オブジェクトを保持するセルオブジェクト だ、という点に注意してほしい。

func2 を実行する前に、func2 が参照している変数が func 内で変更されても、該当するセルオブジェクトが参照しているオブジェクトが書き換えられるだけである。この更新されたセルオブジェクトは func2 でもローカル変数のテーブルに使用されており、func2 実行時には、func2 生成時の値ではなく、func2 を呼び出した瞬間の値を参照することができるのである。

なお、セルオブジェクトは関数のソースをコンパイルした結果に従って生成される。このような仕組み上、クロージャが参照することができるのは、コンパイル時にローカル変数として静的に定義されている変数だけだ。こんな感じで実行時にローカル変数を作成して、その値をクロージャから参照しようとするとエラーとなってしまう。

>>> def a():
...     x = 1
...     y = 2
...     z = 3
...     exec "xxxxx=100"
...     def b():
...         print x, y, z, xxxxx
...     return b
...
  File "<stdin>", line 5
SyntaxError: unqualified exec is not allowed in function
'a' it contains a nested function with free variables

2010/11/15 更新 - 大昔の癖で、locals()でローカル変数を参照していたサンプルを修正。pokarimさん、ありがとうございました。