(ラムダ)関数クロージャは何をキャプチャしますか?


249

最近、私はPythonをいじり始めましたが、クロージャーが機能する方法に変わったものを見つけました。次のコードを検討してください。

adders=[0,1,2,3]

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の関数は最後の値を参照してくださいi3で、その結果adders[1](3)を返します6

次のことについて不思議に思います:

  • クロージャは正確に何をキャプチャしますか?
  • lambda現在の値をキャプチャして、値を変更してiも影響を受けないように関数を説得する最もエレガントな方法は何iですか?

35
UIコードでこの問題が発生しました。私を狂わせた。秘訣は、ループが新しいスコープを作成しないことを覚えておくことです。
detly

3
@TimMB i名前空間を残すにはどうすればよいですか?
2013年

3
@detlyまあprint i、ループの後でそれは機能しないと言っていました。しかし、私はそれを自分でテストし、今、私はあなたが何を意味するかを見ます-それは機能します。ループ変数がpythonのループ本体の後に残っているとは思いもしませんでした。
Tim MB

1
@TimMB-ええ、それは私が意味したことです。以下のための同じifwithtryなど
detly

回答:


161

あなたの2番目の質問には答えましたが、最初の質問については:

クロージャは正確に何をキャプチャしますか?

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)]

1
マックス、他の(簡単な質問)の回答を追加すると、これを承認済みの回答としてマークできます。どうも!
ボアズ

3
Pythonには静的スコープがあります。動的スコープではありません。すべての変数が参照であるだけなので、変数を新しいオブジェクトに設定すると、変数自体(参照)は同じ場所になりますが、別の場所を指します。同じことがSchemeでも起こりますset!。動的スコープの実際については、voidspace.org.uk / python / articles / code_blocks.shtmlを参照してください。
Claudiu

6
オプション2は、関数型言語が「カリー化された関数」と呼ぶものに似ています。
Crashworks、2011

205

デフォルト値の引数を使用して、変数のキャプチャを強制できます。

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

アイデアは(巧妙という名前のパラメータを宣言することですi)、それにあなたが(の値をキャプチャしたい変数のデフォルト値を与えます i


7
デフォルト値を使用する場合は+1。ラムダが定義されるときに評価されるため、ラムダはこの用途に最適です。
クワーニアン

21
+1は、これが公式FAQによって承認されソリューションであるためです。
abarnert 2014年

23
これは素晴らしいです。ただし、デフォルトのPythonの動作はそうではありません。
Cecil Curry

1
これは良い解決策のようには思えませんが、実際には変数のコピーを取得するためだけに関数のシグネチャを変更しています。また、関数を呼び出すものはi変数を混乱させることができますよね?
David Callanan

@DavidCallananラムダについて話します。SDK全体で共有するものではなく、穴を埋めるために独自のコードで通常定義するアドホック関数の一種です。より強力な署名が必要な場合は、実際の関数を使用する必要があります。
エイドリアンプリソン

33

完全を期すために、2番目の質問に対するもう1つの回答:functoolsモジュールでpartialを使用できます。

Chris Lutzが提案したようにadd fromオペレーターをインポートすると、例は次のようになります。

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

次のコードを検討してください。

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

ほとんどの人はこれがまったく混乱しないと思います。予想される動作です。

それで、なぜ人々はそれがループで行われるときそれが異なるだろうと思いますか?自分で間違いを犯したことは知っていますが、理由はわかりません。ループですか?それともラムダ?

結局のところ、ループは以下の短いバージョンにすぎません。

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

11
他の多くの言語ではループが新しいスコープを作成できるため、これがループです。
10

1
この答えはi、ラムダ関数ごとに同じ変数にアクセスする理由を説明しているので、適切です。
デビッドカラナン

3

2番目の質問への回答として、これを行う最もエレガントな方法は、配列の代わりに2つのパラメーターを取る関数を使用することです。

add = lambda a, b: a + b
add(1, 3)

ただし、ここでラムダを使用するのは少しばかげています。Pythonはoperator、基本的な演算子への機能的なインターフェイスを提供するモジュールを提供します。上記のラムダには、加算演算子を呼び出すだけで不要なオーバーヘッドがあります。

from operator import add
add(1, 3)

言語を探索しようとして遊んでいるのはわかりますが、Pythonのスコープの奇妙さが邪魔になる関数の配列を使用する状況は想像できません。

必要に応じて、配列インデックス構文を使用する小さなクラスを作成できます。

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
もちろん、クリス、上記のコードは私の元の問題とは何の関係もありません。これは、私のポイントを簡単に説明するために作成されました。もちろん、それは無意味で愚かです。
ボアズ

3

ここでは、クロージャーのデータ構造とコンテンツを強調表示する新しい例を示します。これにより、囲んでいるコンテキストがいつ「保存」されたかが明確になります。

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

閉鎖には何がありますか?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

特に、my_strはf1のクロージャにありません。

f2のクロージャには何がありますか?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

(メモリアドレスから)両方のクロージャに同じオブジェクトが含まれていることに注意してください。だから、あなたは始めることができますスコープへの参照を持つものとしてラムダ関数を考えること。ただし、my_strはf_1またはf_2のクロージャ内にはなく、iはf_3のクロージャ内にもありません(図には示されていません)。これは、クロージャオブジェクト自体が個別のオブジェクトであることを示唆しています。

クロージャーオブジェクト自体は同じオブジェクトですか?

>>> print f_1.func_closure is f_2.func_closure
False

NB出力int object at [address X]>は、クロージャーが[アドレスX] AKA参照を格納していると思いました。ただし、変数がラムダステートメントの後に再割り当てされると、[アドレスX]が変更されます。
ジェフ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.