lambdaとクロージャに関して
- yattの日記: 無名関数のリストのうち一つを呼び出す無名関数のリストの混乱しやすい挙動 (http://d.hatena.ne.jp/yatt/20091012/1255353971)
fs = [lambda: 1, lambda: 2, lambda: 3]
gs = [lambda: f() for f in fs]
print gs
# => [at 0x6b2f0>, at 0x6b370>, at 0x6b3b0>]
print [g() for g in gs] # expect [1,2,3]
# => [3, 3, 3] # ?!
gs = [lambda: fs[i] for i in xrange(len(fs))]
print [g() for g in gs] #
# => [3, 3, 3] # ...
これは、単なるクロージャの仕様と思われる。以下と同じ。
>>> def foo(): ... L = [] ... for i in range(3): ... L.append(lambda: i) ... return L ... >>> g = foo() >>> g[0](), g[1](), g[2]() (2, 2, 2)
つまり、gsに代入されるリスト内包表記内で、fはローカル変数に当たる。リスト内包表記でリストが作成されれば、ローカル変数fはスコープ外となる(追記:2.5までは、リスト内包を抜けた後もfはローカル変数として生き残る)。つまり、gsは、lambda: f()のリストとなるが、ここでキャプチャされるfは、最後に評価されたfとなる。これは上記の例と同様な挙動。つまり、クロージャで1つの変数を参照しているので、1つの変数しかキャプチャされない。
C#では、以下のように新たなローカル変数jを介せば、0,1,2と変化するみたいだが、Pythonでは以下のように上記の例と同様な挙動となる。こういう部分は、言語によって若干挙動が異なると思われる。回避方法は、yattさんが書いておられる通り、引数として渡すのが確実。
>>> def foo(): ... L = [] ... for i in range(3): ... def bar(): ... j = i ... return j ... L.append(bar) ... return L ... >>> g = foo() >>> g[0](), g[1](), g[2]() (2, 2, 2)
結論として、クロージャとループを組み合わせる場合、どの変数がキャプチャされるかを注意しておく必要があると言えると思う。