字句クロージャはどのように機能しますか?
-
04-07-2019 - |
質問
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 * 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コードは正しく動作します:
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)
" 0 2 4"印刷されます。
違いを説明してもらえますか?
更新:
i
がグローバルであるという問題はではありません。これは同じ動作を示します:
<*>
コメント行が示すように、その時点では i
は不明です。それでも、「4 4 4」と印刷されます。
[0]});
}
foreach my $f (@flist)
{
print $f->(2), "\n";
}
&quot; 0 2 4&quot;印刷されます。
違いを説明してもらえますか?
更新:
i
がグローバルであるという問題はではありません。これは同じ動作を示します:
コメント行が示すように、その時点では i
は不明です。それでも、「4 4 4」と印刷されます。
解決
Pythonは実際には定義どおりに動作しています。 3つの個別の関数が作成されますが、それぞれに定義されている環境の閉鎖があります。この場合、グローバル環境(または、ループは別の関数内に配置されます)。ただし、これはまさに問題です。この環境では、 iが変更されており、クロージャーはすべて同じi を参照しています。
これが私が思いつく最良の解決策です-関数クリエーターを作成し、代わりに that を呼び出します。これにより、作成された各機能に異なる環境が強制され、各機能に異なる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)
これは、副作用と関数型プログラミングを組み合わせたときに起こることです。
他のヒント
ループで定義された関数は、値が変化する間、同じ変数 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)
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を出力します。
これを見てください:
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))
起きているのは、変数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)を取り、ループの本体で定義された本体を持つラムダを定義します
- パラメーターとしてiの適切な値を使用した、そのラムダの即時呼び出し。
ie。以下の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変数を共有するだけです。
一部の言語でこれが何らかの方法で機能する理由と、別の方法で機能する理由をまだ完全には確信していません。 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)))
印刷&quot; 6 6 6&quot; (ここでリストは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)
印刷&quot; 6 4 2&quot;
そして、すでに述べたように、JavascriptはPython / CLキャンプに入っています。ここには実装の決定があり、異なる言語が異なる方法でアプローチしているようです。決定が何であるかを正確に理解したいと思います。
問題は、すべてのローカル関数が同じ環境、つまり同じ i
変数にバインドすることです。解決策(回避策)は、関数(またはラムダ)ごとに個別の環境(スタックフレーム)を作成することです。
t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]
>>> t[1](2)
2
>>> t[2](2)
4
変数 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 で関数を呼び出したときの値は変わりませんcode>。
この振る舞いの背後にある理由はすでに説明されており、複数の解決策が投稿されていますが、これは最もPython的だと思います(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の答えは、正直に言うとハックです。引数にデフォルト値を設定します(正常に機能しますが、&quot; pythonic&quot;ではありません)。