ネストスコープのループ

内側の変数を代入しただけだと、その変数自体が次々に変化していくため、期待した動作が得られない。

>>> def foo(x):
...     result = []
...     for i in range(x):
...             result.append(lambda : print(i))
...     return result
...
>>> lst = foo(3)
>>> for l in lst: l()
...
2
2
2

上記の場合、012となってほしいところが、printされた値は全て最後に代入された2になっている。
これを回避する方法として、デフォルト値を利用するやり方がある。

>>> def foo(x):
...     result = []
...     for i in range(x):
...             result.append(lambda y=i: print(y) )
...     return result
...
>>> lst = foo(3)
>>> for l in lst: l()
...
0
1
2

javascriptにおけるクロージャによる解決

似たような話を、JavaScriptでも読んだ。(「JavaScript The Good Parts」)

var add_clickEvent = function(ns){
    var i;
    for(i = 0; i < nodes.length; i++){
      nodes[i].onclick = function(){
	alert(i);
      };
    }
};
add_clickEvent(document.getElementsByTagName("button"));

全てのボタンのonclickに、ボタンの順序(インデックス)に従ったメッセージを出すことを意図している。
しかし結果としては、全てのonclickはボタンの総数をメッセージ表示する。
これは、ハンドラ関数は変数iを処理しているのであって、その時点のインデックスの値を処理しているわけではないから。


扱おうと試みた時点では、その変数は期待した状態になっていないという点で似ている。
回避策として、クロージャの応用で、ハンドラ関数を返す無名関数を即時実行する方法が記載されていた。

var add_clickEvent = function(nodes){
  var i;
  for(i = 0; i < nodes.length; i++){
    nodes[i].onclick = function(j){
      return function(e){
	alert(j);
      };
    }(i);
  }
};
add_clickEvent(document.getElementsByTagName("button"));

無名関数が扱うパラメータjは、add_ClickEventで定義されたiとは異なる変数になり、この無名関数のjを参照したハンドラ関数がonclickに設定される。

Pythonクロージャの方法を試してみる

これなら、同じようなことをPythonでもできるかなと試してみたら、できた。

>>> def foo(x):
...     result = []
...     for i in range(x):
...             result.append(
...                     (lambda j:
...                             lambda: print(j))(i))
...     return result
...
>>> lst = foo(3)
>>> for l in lst: l()
...
0
1
2

うーん…手元で動くというだけで、自信がない。
やりたいこと自体は「時点の変数の値を保持する」だけなので、デフォルト引数の特性を知っているならあえてそれを使わない手もないな、と思った。