web-dev-qa-db-ja.com

ラムダ関数とそのパラメーターの範囲?

一連のguiイベントとほぼ同じコールバック関数が必要です。関数は、どのイベントがそれを呼び出したかによって、わずかに異なる動作をします。私には単純なケースのように思えますが、ラムダ関数のこの奇妙な動作を理解することはできません。

だから私は以下の簡単なコードを持っています:

def callback(msg):
    print msg

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(m))
for f in funcList:
    f()

#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
    f()

このコードの出力は次のとおりです。

mi
mi
mi
do
re
mi

私は期待した:

do
re
mi
do
re
mi

なぜイテレーターを使うと物事が台無しになるのですか?

ディープコピーを使用してみました:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

しかし、これには同じ問題があります。

76
agartland

ここでの問題は、m変数(参照)が周囲のスコープから取得されることです。パラメーターのみがラムダスコープに保持されます。

これを解決するには、ラムダの別のスコープを作成する必要があります。

def callback(msg):
    print msg

def callback_factory(m):
    return lambda: callback(m)

funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(callback_factory(m))
for f in funcList:
    f()

上記の例では、lambdaはmを見つけるためにサラウンドスコープも使用しますが、今回はcallback_factoryスコープごとに1回作成されるcallback_factory呼び出し。

または functools.partial で:

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()
70
lispmachine

ラムダが作成されると、ラムダは使用する囲みスコープ内の変数のコピーを作成しません。後で変数の値を検索できるように、環境への参照を保持します。 mは1つだけです。ループを通じて毎回割り当てられます。ループの後、変数mの値は'mi'。したがって、後で作成した関数を実際に実行すると、それが作成した環境でmの値が検索され、それまでに値'mi'

この問題に対する一般的で慣用的な解決策の1つは、ラムダが作成された時点でmの値をキャプチャし、それをオプションのパラメーターのデフォルト引数として使用することです。通常、同じ名前のパラメーターを使用するため、コードの本文を変更する必要はありません。

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
120
newacct

Pythonはもちろん参照を使用しますが、このコンテキストでは重要ではありません。

ラムダ(または、これはまったく同じ動作なので、関数)を定義する場合、実行前にラムダ式を評価しません。

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

あなたのラムダの例よりさらに驚くべきこと:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

要するに、動的であると考えてください。解釈の前に何も評価されないため、コードはmの最新の値を使用します。

ラムダ実行でmを探すとき、mは最上位のスコープから取得されます。これは、他の人が指摘したように、別のスコープを追加することにより、この問題を回避できます。

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

ここで、ラムダが呼び出されると、ラムダの定義スコープでxが検索されます。このxは、ファクトリーの本体で定義されたローカル変数です。このため、ラムダ実行で使用される値は、ファクトリーの呼び出し中にパラメーターとして渡された値になります。そしてドレミ!

注として、ファクトリをfactory(m)[xをmで置換]として定義することもできますが、動作は同じです。わかりやすくするために別の名前を使用しました:)

Andrej Bauer が同様のラムダ問題を抱えていることに気付くかもしれません。このブログで興味深いのはコメントです。ここでは、python閉鎖:)について詳しく学ぶことができます。

6
Nicolas Dumazet

目の前の問題とは直接関係ありませんが、それでも貴重な知恵です。 Python Objects by Fredrik Lundh。

1
tzot

実際にはPythonには古典的な意味での変数はなく、適切なオブジェクトへの参照によってバインドされた名前だけがあります。関数でさえPythonのある種のオブジェクトであり、ラムダはルールの例外を作りません:)

0
Tom

補足説明として、mapは、よく知られたPython図によって軽spされていますが、この落とし穴を防ぐ構造を強制します。

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

注意:最初のlambda iは、他の回答では工場のように機能します。

0
YvesgereY

まず、表示されているものは問題ではなく、参照による呼び出しや値による呼び出しとは関係ありません。

定義したラムダ構文にはパラメーターがないため、パラメーターmで表示されるスコープはラムダ関数の外部にあります。これがあなたがこれらの結果を見ている理由です。

あなたの例では、ラムダ構文は必要ありません。むしろ、単純な関数呼び出しを使用します。

for m in ('do', 're', 'mi'):
    callback(m)

繰り返しになりますが、使用しているラムダパラメーターと、そのスコープの正確な開始位置と終了位置について、非常に正確である必要があります。

補足として、パラメーターの受け渡しについて。 pythonのパラメーターは常にオブジェクトへの参照です。AlexMartelliを引用するには:

用語の問題は、Pythonでは、名前の値がオブジェクトへの参照であるという事実による可能性があります。したがって、常に値を渡し(暗黙的なコピーは行わない)、その値は常に参照です。 [...]ここで、「オブジェクト参照による」、「コピーされていない値による」などの名前を付けたい場合は、私のゲストになります。 「変数はボックス」である言語から「変数はポストイットタグ」である言語に、より一般的に適用される用語を再利用しようとすると、助けになるよりも混乱しやすくなります。

0
Yuval Adam

変数mはキャプチャされているため、ラムダ式は常に「現在の」値を参照します。

ある時点で値を効果的にキャプチャする必要がある場合は、必要な値をパラメーターとして受け取り、ラムダ式を返す関数を作成します。その時点で、ラムダはparameter's値をキャプチャします。この値は、関数を複数回呼び出しても変更されません。

def callback(msg):
    print msg

def createCallback(msg):
    return lambda: callback(msg)

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(createCallback(m))
for f in funcList:
    f()

出力:

do
re
mi
0
Jon Skeet

はい、それはスコープの問題です。ラムダまたはローカル関数を使用しているかどうかにかかわらず、外側のmにバインドします。代わりに、ファンクターを使用します。

class Func1(object):
    def __init__(self, callback, message):
        self.callback = callback
        self.message = message
    def __call__(self):
        return self.callback(self.message)
funcList.append(Func1(callback, m))
0
Benoît