web-dev-qa-db-ja.com

字句クロージャはどのように機能しますか?

Javascriptコードの字句クロージャに関する問題を調査しているときに、Pythonでこの問題に遭遇しました。

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

この例では、lambdaを慎重に避けていることに注意してください。 「4 4 4」と印刷されますが、これは驚くべきことです。 「0 2 4」が期待されます。

この同等のPerlコードは正しく動作します。

my @flist = ();

foreach my $i (0 .. 2)
{
    Push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

「0 2 4」が印刷されます。

違いを説明してもらえますか?


更新:

問題はではないiがグローバルであることです。これは同じ動作を示します:

flist = []

def outer():
    for i in xrange(3):
        def inner(x): return x * i
        flist.append(inner)

outer()
#~ print i   # commented because it causes an error

for f in flist:
    print f(2)

コメント行が示すように、iはその時点では不明です。それでも、「4 4 4」と出力されます。

143
Eli Bendersky

Pythonは実際には定義どおりに動作しています。 3つの別々の機能 作成されますが、それぞれに 定義されている環境の閉鎖 -この場合、グローバル環境(またはループが別の関数内に配置されている場合は外部関数の環境)。これはまさに問題ですが、この環境では、 私は変異しています、およびクロージャーすべて 同じiを参照

ここに私が思いつくことができる最高のソリューションがあります-関数クリエーターを作成して呼び出します それ 代わりに。これは強制します 異なる環境 作成された関数ごとに、 違う私 それぞれに。

flist = []

for i in xrange(3):
    def funcC(j):
        def func(x): return x * j
        return func
    flist.append(funcC(i))

for f in flist:
    print f(2)

これは、副作用と関数型プログラミングを混在させると発生します。

148
Claudiu

ループで定義された関数は、値が変化する間、同じ変数iにアクセスし続けます。ループの最後で、すべての関数は同じ変数を指し、ループ内の最後の値を保持しています。効果は例で報告されたものです。

iを評価してその値を使用するための一般的なパターンは、それをパラメーターのデフォルトとして設定することです。パラメーターのデフォルトは、defステートメントの実行時に評価されます。変数は固定されています。

以下は期待どおりに機能します。

flist = []

for i in xrange(3):
    def func(x, i=i): # the *value* of i is copied in func() environment
        return x * i
    flist.append(func)

for f in flist:
    print f(2)
153
piro

functoolsライブラリーを使用してこれを行う方法は次のとおりです(質問が提起された時点で利用可能かどうかはわかりません)。

from functools import partial

flist = []

def func(i, x): return x * i

for i in xrange(3):
    flist.append(partial(func, i))

for f in flist:
    print f(2)

期待どおりに出力0 2 4。

31
Luca Invernizzi

これを見てください:

for f in flist:
    print f.func_closure


(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

これは、すべて同じi変数インスタンスを指し、ループが終了すると値が2になることを意味します。

読みやすい解決策:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))
13
Null303

起こっているのは、変数iがキャプチャされ、関数が呼び出されたときにバインドされている値を返しているということです。関数型言語では、私はリバウンドしないので、この種の状況は発生しません。ただし、Pythonでは、LISPでも見たように、これはもはや事実ではありません。

スキームの例との違いは、doループのセマンティクスを使用することです。 Schemeは、他の言語のように既存のiバインディングを再利用するのではなく、ループを通るたびに新しいi変数を効果的に作成しています。ループの外部で作成された別の変数を使用してそれを変更すると、スキームで同じ動作が見られます。ループを次のものに置き換えてみてください。

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

詳細については、 こちら をご覧ください。

[編集]おそらくそれを記述するより良い方法は、doループを次のステップを実行するマクロと考えることです:

  1. ループの本体で定義された本体を使用して、単一のパラメーター(i)を取るラムダを定義します。
  2. パラメータとしてiの適切な値を使用した、そのラムダの即時呼び出し。

すなわち。以下のpythonと同等:

flist = []

def loop_body(i):      # extract body of the for loop to function
    def func(x): return x*i
    flist.append(func)

map(loop_body, xrange(3))  # for i in xrange(3): body

Iは親スコープからのものではなく、独自のスコープ内の新しい変数(ラムダへのパラメーター)であるため、観察する動作を取得できます。 Pythonはこの暗黙の新しいスコープを持たないため、forループの本体はi変数を共有するだけです。

7
Brian

一部の言語でこれが一方向に機能する理由と、他の方法で機能する理由をまだ完全に確信しているわけではありません。 Common LISPでは、Pythonのようなものです。

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist* 
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)  
  (format t "~a~%" (funcall f 2)))

「6 6 6」を出力します(ここでリストは1から3であり、逆に構築されていることに注意してください)。SchemeではPerlのように動作します:

(define flist '())

(do ((i 1 (+ 1 i)))
    ((>= i 4))
  (set! flist 
    (cons (lambda (x) (* i x)) flist)))

(map 
  (lambda (f)
    (printf "~a~%" (f 2)))
  flist)

「6 4 2」を印刷します

そして、すでに述べたように、JavascriptはPython/CLキャンプに入っています。ここでは実装の決定があり、異なる言語が異なる方法でアプローチしているようです。決定が何であるかを正確に理解したいと思います。

4
Eli Bendersky

問題は、すべてのローカル関数が同じ環境、つまり同じi変数にバインドすることです。解決策(回避策)は、関数(またはラムダ)ごとに個別の環境(スタックフレーム)を作成することです。

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4
2
Rafał Dowgird

変数iはグローバルであり、その値は関数fが呼び出されるたびに2です。

次のように、あなたが求めている動作を実装したいと思います。

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
... 
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

更新への応答:この動作を引き起こしているのは、i自体のグローバル性ではないそれは、fが呼び出されたときに固定値を持つ囲みスコープの変数であるという事実。 2番目の例では、iの値はkkk関数のスコープから取得され、flistで関数を呼び出したときは何も変わりません。

1
Alex Coventry

動作の背後にある理由はすでに説明されており、複数の解決策が投稿されていますが、これは最もPythonicだと思います(Pythonのすべてがオブジェクトです!):

flist = []

for i in xrange(3):
    def func(x): return x * func.i
    func.i=i
    flist.append(func)

for f in flist:
    print f(2)

Claudiuの答えは関数ジェネレーターを使用してかなり良いですが、piroの答えはハックです。正直に言うと、iをデフォルト値で「隠された」引数にしています(うまく動作しますが、「Pythonic」ではありません) 。

0
darkfeline