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


92

一連の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()

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


3
あなたの質問のタイトルはやや誤解を招くものです。
lispmachine 2009年

1
混乱するのにラムダを使用するのはなぜですか?defを使用して関数を定義してみませんか?ラムダを非常に重要にする問題についてはどうですか?
S.Lott 2009年

@ S.Lottネストされた関数は、同じ問題を
引き起こし

1
@agartland:あなたは私ですか?私もGUIイベントに取り組んでいた、と私は背景調査の際に、このページを見つける前に、次のとほぼ同一のテストを書いた:pastebin.com/M5jjHjFT
imallett

5
異なる値を持つループで定義されたラムダがすべて同じ結果を返すのはなぜですか?を参照してくださいPythonの公式プログラミングFAQにあります。それは問題をかなりうまく説明し、解決策を提供します。
abarnert 2014年

回答:


80

ここでの問題は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()

上記の例では、ラムダもサラウンドスコープを使用してを検索しますmが、今回callback_factorycallback_factory 呼び出しごとに1回作成されるスコープです。

または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()

2
この説明は少し誤解を招く可能性があります。問題は、スコープではなく、反復でのmの値の変更です。
ixx 2012年

その上のコメントは、フェノニモンと解決策を説明するリンクも示されている質問のコメントで@abarnertが指摘しているように、真実です。ファクトリメソッドは、ファクトリメソッドの引数がラムダに対してローカルなスコープを持つ新しい変数を作成する効果があるのと同じ効果を提供します。ただし、ラムダへの引数がないため、指定されたsolitionは構文的に機能しません。また、以下のラムダソリューションのラムダは、ラムダを作成するための新しい永続的なメソッドを作成せずに同じ効果を提供します
Mark Parris 2010年

134

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

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

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

6
いい解決策!トリッキーですが、元の意味は他の構文よりも明確だと思います。
Quantum7 2009年

3
これについては、ハックやトリッキーなことは何もありません。これは、公式のPythonFAQが提案しているのとまったく同じソリューションです。こちらをご覧ください
abarnert 2014年

3
@abernert、「ハックでトリッキー」は、「公式のPythonFAQが提案するソリューションであること」と必ずしも互換性がないわけではありません。参照していただきありがとうございます。
ドンハッチ

1
同じ変数名を再利用することは、この概念に慣れていない人にはわかりません。ラムダn = mの場合、図はより良いでしょう。はい、コールバックパラメータを変更する必要がありますが、forループの本体は同じままである可​​能性があります。
ニック

1
ずっと+1!これが解決策になるはずです...受け入れられた解決策はこれほど良くはありません。結局のところ、ここではラムダ関数を使用しようとしています...単にすべてをdef宣言に移動するだけではありません。
255.tar.xz

6

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をfactory(m)[xをmに置き換える]と定義することもできますが、動作は同じです。わかりやすくするために別の名前を使用しました:)

AndrejBauerが同様のラムダ問題を抱えていることに気付くかもしれません。そのブログで興味深いのはコメントです。ここでは、Pythonクロージャについて詳しく学びます:)


1

目前の問題に直接関係しているわけではありませんが、それでも非常に貴重な知恵です。FredrikLundhによるPythonオブジェクトです。


1
直接あなたの答えに関連しますが、子猫を検索しません:google.com/search?q=kitten
Singletoned

@Singletoned:OPが私がリンクを提供した記事を悪用した場合、彼らはそもそも質問をしませんでした。それが間接的に関連している理由です。子猫が私の答えに間接的にどのように関係しているかを私に説明してくれると確信しています(全体論的なアプローチを通じて、私は推測します;)
tzot 2011年

1

はい、それはスコープの問題です。ラムダ関数を使用しているかローカル関数を使用しているかに関係なく、外側の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))

1

ラムダへのソリューションはよりラムダです

In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]

In [1]: funcs
Out[1]: 
[<function __main__.<lambda>>,
 <function __main__.<lambda>>,
 <function __main__.<lambda>>]

In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']

アウターは、lambda現在の値に結合するために使用されるiのをj

外側のたびlambdaに呼び出され、内側のインスタンスを作成lambdaしてj現在の値にバインドiとしてiの値


0

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

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

この例では、Lambda構文は必要ありません。むしろ、単純な関数呼び出しを使用することをお勧めします。

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

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

補足として、パラメータの受け渡しについて。Pythonのパラメーターは、常にオブジェクトへの参照です。アレックス・マルテッリを引用するには:

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


0

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

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

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

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


「古典的な意味で」と言うときは、「Cのように」という意味です。Pythonの言語を含め、たっぷり、C.とは異なる変数を実装するには
ネッドBatchelder

0

注意点として、mapいくつかのよく知られたPythonの数字によって軽蔑ものの、この落とし穴を防止工事を強制します。

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.