1. ホーム
  2. パイソン

[解決済み】ラムダ関数のクロージャは何を捕捉するのか?

2022-03-28 23:04:35

質問

最近、Pythonをいじり始めて、クロージャの動作に奇妙なことに気がつきました。以下のコードを考えてみてください。

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

これは、一つの入力を受け取り、その入力に数値を加えて返す関数の簡単な配列を構築する。この関数は for ループで、イテレータ i から実行されます。 0 から 3 . これらの各数値に対して lambda をキャプチャする関数が作成されます。 i を作成し、この関数の入力に追加します。最後の行では、2番目の lambda 関数に 3 をパラメータとする。驚いたことに、出力は 6 .

を期待したのですが 4 . 私の理由は、Pythonではすべてがオブジェクトであり、したがってすべての変数はそれへのポインタであることが不可欠だからです。そのため lambda のクロージャは i が現在指している整数オブジェクトへのポインタを格納すると思っていました。 i . つまり i に新しい整数オブジェクトを割り当てても、以前に作成したクロージャに影響を与えないはずです。悲しいことに adders の配列は、デバッガで見ると、そのように見える。すべて lambda の最後の値を参照します。 i , 3 となり、その結果 adders[1](3) を返します。 6 .

ということが気になりますね。

  • クロージャは具体的に何を捕捉するのか?
  • を納得させる最もエレガントな方法は何でしょうか? lambda の現在値を取得するための関数です。 i を実行したときに影響を受けないような方法で i は、その値を変更するのですか?

解決方法は?

2つ目の質問については回答済みですが、1つ目の質問については。

クロージャは具体的に何を捕らえるのでしょうか?

Pythonのスコーピングは <ストライク ダイナミックで レキシカルです。クロージャは常に変数の名前とスコープを記憶し、それが指しているオブジェクトは記憶しません。この例では、すべての関数が同じスコープで作成され、同じ変数名を使用しているので、常に同じ変数を参照していることになります。

もう一つの質問である、これを克服する方法についてですが、思い当たるのは2つの方法です。

  1. 最も簡潔な方法ですが、厳密には同等ではありません。 Adrien Plisson が推奨する . ラムダを追加で作成し、その追加引数のデフォルト値を保存したいオブジェクトに設定します。

  2. もう少し冗長ですが、ラムダを作成するたびに新しいスコープを作成する方が簡単でしょう。

     >>> adders = [0,1,2,3]
     >>> for i in [0,1,2,3]:
     ...     adders[i] = (lambda b: lambda a: b + a)(i)
     ...     
     >>> adders[1](3)
     4
     >>> adders[2](3)
     5
    
    

ここでのスコープは、引数を束ねる新しい関数(簡潔にするためにラムダ)を使って作成し、束ねたい値を引数として渡しています。しかし、実際のコードでは、ラムダの代わりに普通の関数を使って新しいスコープを作成することがほとんどです。

def createAdder(x):
    return lambda y: y + x
adders = [createAdder(i) for i in range(4)]