字句閉鎖はどのように機能しますか?


149

私は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」と印刷します。



3
これはこの問題に関するかなり良い記事です。me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

回答:


151

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)

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


5
あなたのソリューションはJavascriptで使用されるものでもあります。
Eli Bendersky 2008年

9
これは誤動作ではありません。定義どおりに動作しています。
Alex Coventry

6
IMO PIROよりよい解決策を持っているstackoverflow.com/questions/233673/...
JFS

2
わかりやすくするために、おそらく最も内側の「i」を「j」に変更します。
eggsyntax

7
何についてちょうどこのようにそれを定義:def inner(x, i=i): return x * i
dashesy

152

ループで定義された関数は、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)

7
s /コンパイル時/ defステートメントが実行された瞬間/
jfs 2008年

23
これは独創的な解決策であり、恐ろしいものです。
Stavros Korokithakis

このソリューションには1つの問題があります。funcには2つのパラメーターがあります。つまり、可変量のパラメーターでは機能しません。さらに悪いことに、2番目のパラメーターを指定してfuncを呼び出すとi、定義の元のファイルが上書きされます。:-(
Pascal

34

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を出力します。


私はこれを本当に使いたかったのですが、私の関数は実際にはクラスメソッドであり、渡された最初の値はselfです。とにかくこれの周りにありますか?
Michael David Watson

1
もちろんです。add(self、a、b)メソッドを含むMathクラスがあり、a = 1を設定して 'increment'メソッドを作成するとします。次に、クラス「my_math」のインスタンスを作成します。インクリメントメソッドは「increment = partial(my_math.add、1)」になります。
Luca Invernizzi 2013年

2
この手法をメソッドに適用するにfunctools.partialmethod()は、python 3.4以降でも使用できます
Matt Eding

13

これを見てください:

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))

1
私の質問はより「一般的」です。なぜPythonにこの欠陥があるのですか?私はレキシカルクロージャーをサポートする言語(PerlやLisp王朝全体など)がこれを正しく機能することを期待しています。
Eli Bendersky 2008年

2
何かが欠陥を持っている理由を尋ねることは、それが欠陥ではないと想定しています。
Null303、2008年

7

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

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

(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変数を共有するだけです。


面白い。doループのセマンティクスの違いに気づきませんでした。ありがとう
Eli Bendersky 2008年

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キャンプにあります。ここでは、異なる言語が異なる方法でアプローチする実装決定があるようです。私は決定が何であるかを正確に理解したいと思います。


8
違いは、スコープルールではなく(do ...)です。スキームでは、ループを通過するたびに新しい変数を作成しますが、他の言語は既存のバインディングを再利用します。詳細とlisp / pythonに類似した動作のスキームバージョンの例については、私の回答を参照してください。
ブライアン、

2

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

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

>>> t[1](2)
2
>>> t[2](2)
4

1

変数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関数を呼び出しても何も変わりませんflist


0

動作の背後にある理由はすでに説明されており、複数のソリューションが投稿されていますが、これは最も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の答えはハックです。正直に言うと、デフォルトの値で「非表示」の引数になっているためです(うまくいきますが、「pythonic」ではありません)。 。


私はそれはあなたのPythonバージョンに依存すると思います。今、私はより経験を積んでおり、この方法を提案することはもうありません。Claudiuは、Pythonでクロージャーを作成する適切な方法です。
darkfeline 2013年

1
これは、Python 2または3では機能しません(どちらも「4 4 4」を出力します)。func中にはx * func.i常に定義された最後の関数を参照します。そのため、各関数には個別に正しい番号が付いていますが、いずれにしても、最後の関数から読み取っています。
ラムダフェアリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.