34

関数シグネチャから変数を削除するためにクロージャを使用しようとしています (アプリケーションは、値を格納するディクショナリに大量のパラメータを制御するインターフェイスの Qt 信号を接続するために必要なすべての関数を作成することです)。

lambda別の関数でラップされていない関数を使用すると、すべてのケースで姓が返される理由がわかりません。

names = ['a', 'b', 'c']

def test_fun(name, x):
    print(name, x)

def gen_clousure(name):
    return lambda x: test_fun(name, x)

funcs1 = [gen_clousure(n) for n in names]
funcs2 = [lambda x: test_fun(n, x) for n in names]

# this is what I want
In [88]: for f in funcs1:
   ....:     f(1)
a 1
b 1
c 1

# I do not understand why I get this
In [89]: for f in funcs2:
   ....:     f(1)
c 1
c 1
c 1
4

1 に答える 1

62

その理由は、クロージャー (ラムダまたはその他) が値ではなく名前を閉じるためです。を定義するlambda x: test_fun(n, x)と、 n は関数内にあるため評価されません。関数が呼び出されたときに評価され、その時点でそこにある値はループからの最後の値です。

冒頭で「クロージャを使用して関数シグネチャから変数を削除したい」と言いますが、実際にはそのようには機能しません。(ただし、「削除」の意味に応じて、満足できる方法については以下を参照してください。)関数本体内の変数は、関数が定義されているときに評価されません。関数が関数定義時に存在する変数の「スナップショット」を取得するには、変数を引数として渡す必要があります。これを行う通常の方法は、デフォルト値が外側のスコープからの変数である引数を関数に与えることです。これら 2 つの例の違いを見てください。

>>> stuff = [lambda x: n+x for n in [1, 2, 3]]
>>> for f in stuff:
...     print f(1)
4
4
4
>>> stuff = [lambda x, n=n: n+x for n in [1, 2, 3]]
>>> for f in stuff:
...     print f(1)
2
3
4

2 番目の例では、引数として関数に渡すnと、n の現在の値がその関数に「固定」されます。この方法で値をロックしたい場合は、このようなことをしなければなりません。(このようにしないと、グローバル変数などはまったく機能しません。使用時に自由変数を検索することが不可欠です。)

この動作はラムダに固有のものではないことに注意してください。defを使用して、外側のスコープから変数を参照する関数を定義する場合、同じスコープ規則が有効になります。

本当にそうしたい場合は、返された関数に余分な引数を追加することを避けることができますが、そのためには、次のように、その関数をさらに別の関数でラップする必要があります。

>>> def makeFunc(n):
...     return lambda x: x+n
>>> stuff = [makeFunc(n) for n in [1, 2, 3]]
>>> for f in stuff:
...     print f(1)
2
3
4

ここで、内側のラムダnは呼び出されたときに の値を検索します。しかし、nit が参照するのはグローバル変数ではなく、囲んでいる関数内のローカル変数ですmakeFunc。が呼び出されるたびに、このローカル変数の新しい値が作成されmakeFunc、返されたラムダによって、その呼び出しで有効だったローカル変数値を「保存」するクロージャーが作成されますmakeFunc。したがって、ループで作成された各関数には、 と呼ばれる独自の「プライベート」変数がありますx。(この単純なケースでは、外側の関数にラムダを使用してこれを行うこともできます --- stuff = [(lambda n: lambda x: x+n)(n) for n in [1, 2, 3]]--- しかし、これは読みにくくなります。)

your を引数として渡す必要があることに注意してください。これは、このようにすることで、リストnに入る同じ関数に引数として渡さないということです。stuff代わりに、 に入れたい関数を作成するヘルパー関数に引数として渡しますstuff。この 2 つの関数のアプローチを使用する利点は、返される関数が "クリーン" であり、余分な引数がないことです。これは、多くの引数を受け入れる関数をラップしている場合に役立ちます。この場合、n引数がリスト内のどこにあるかを覚えておくのが混乱する可能性があります。欠点は、このようにすると、別の囲み関数が必要になるため、関数を作成するプロセスがより複雑になることです。

要するに、トレードオフがあります。関数作成プロセスを単純化できます (つまり、2 つのネストされた関数は必要ありません) が、結果の関数をもう少し複雑にする必要があります (つまり、この余分なn=n引数があります) 。 . または、関数をより単純にすることもできます (つまり、n=n 引数を持たない) が、関数作成プロセスをより複雑にする必要があります (つまり、メカニズムを実装するために 2 つのネストされた関数が必要です)。

于 2012-11-13T03:43:26.767 に答える