156

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。意外と「444」と書いてあります。「024」を期待します。

この同等のPerlコードはそれを正しく行います:

my @flist = ();

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

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

「024」が印刷されます。

違いを教えてください。


アップデート:

問題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その時点では不明です。それでも、「444」と表示されます。

4

10 に答える 10

159

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

これが私が思いつく最善の解決策です-関数クリエーターを作成し、代わりにそれを呼び出します。これにより、作成された関数ごとに異なる環境が強制され、それぞれに異なる 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)

これは、副作用と関数型プログラミングを組み合わせると起こることです。

于 2008-10-24T14:47:08.953 に答える
157

ループで定義された関数は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)
于 2008-10-25T01:56:42.237 に答える
36

ライブラリを使用してそれを行う方法は次のとおり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 を出力します。

于 2011-07-24T06:24:40.130 に答える
14

これを見てください:

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))
于 2008-10-24T14:36:07.493 に答える
8

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

スキームの例との違いは、do ループのセマンティクスを使用することです。スキームは、他の言語のように既存の 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 変数を共有するだけです。

于 2008-10-25T11:28:04.720 に答える
4

一部の言語でこれが 1 つの方法で機能し、別の方法で機能する理由については、まだ完全には確信が持てません。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 陣営に属しています。ここには、異なる言語が異なる方法でアプローチする実装の決定があるようです。決定が何であるかを正確に理解したいと思います。

于 2008-10-25T07:20:03.847 に答える
4

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

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

>>> t[1](2)
2
>>> t[2](2)
4
于 2008-10-24T14:42:55.607 に答える
2

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

次のように、あなたが求めている動作を実装する傾向があります。

>>> 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、関数を on 呼び出しても何も変更されませんflist

于 2008-10-24T14:17:35.143 に答える
1

この動作の背後にある理由は既に説明されており、複数の解決策が投稿されていますが、これが最も 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」ではありません)。 .

于 2012-07-24T08:20:04.593 に答える