入れ子関数のローカル変数


105

わかりました、これについては我慢してください。恐ろしく複雑に見えることはわかっていますが、何が起こっているのかを理解するのを手伝ってください。

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

与える:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

それで、基本的に、なぜ私は3つの異なる動物を得ないのですか?cage入れ子関数のローカルスコープに「パッケージ化」されていませんか?そうでない場合、ネストされた関数の呼び出しはどのようにローカル変数を検索しますか?

この種の問題が発生した場合、通常は「間違っている」ことを意味しますが、何が起こるかを理解したいと思います。


1
試してみてくださいfor animal in ['cat', 'dog', 'cow']...誰かがやって来てこれを説明すると確信しています-それはそれらのPythonの落とし穴の1つです:)
Jon Clements

回答:


114

ネストされた関数は、定義時ではなく実行時に親スコープから変数を検索します。

関数本体がコンパイルされ、「フリー」変数(割り当てによって関数自体に定義されていない)が検証され、インデックスを使用して各セルを参照するコードで、クロージャーセルとして関数にバインドされます。pet_functionしたがって、1つの自由変数(cage)があり、クロージャーセル、インデックス0を介して参照さcageget_pettersます。クロージャー自体は、関数内のローカル変数を指します。

実際に関数を呼び出すと、そのクロージャは、関数を呼び出したときにcage周囲のスコープ内のの値を調べるために使用されます。ここに問題があります。関数を呼び出すときまでに、関数は結果の計算をすでに完了しています。その実行中のある時点でのローカル変数は、それぞれ割り当てられていた、との文字列を、関数の最後で、その最後の値が含まれています。したがって、動的に返される各関数を呼び出すと、値が出力されます。get_petterscage'cow''dog''cat'cage'cat''cat'

回避策は、クロージャーに依存しないことです。代わりに部分関数を使用したり、新しい関数スコープを作成したり、変数をキーワードパラメータのデフォルト値としてバインドしたりできます

  • 以下を使用した部分関数の例functools.partial()

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
    
  • 新しいスコープの例を作成する:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
    
  • 変数をキーワードパラメータのデフォルト値としてバインドします。

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))
    

scoped_cageループ内で関数を定義する必要はありません。コンパイルは1回だけ行われ、ループの各反復では行われません。


1
今日は仕事のためのスクリプトでこの壁に3時間頭をぶつけました。あなたの最後のポイントは非常に重要であり、私がこの問題に遭遇した主な理由です。コード全体にクロージャーを備えたコールバックがありますが、同じ手法をループで試すことが私に与えられました。
DrEsperanto 2018年

12

私の理解では、生成されたpet_functionが実際に呼び出されるときに、以前ではなく親関数の名前空間でケージが検索されます。

だからあなたがするとき

funs = list(get_petters())

最後に作成されたケージを見つける3つの関数を生成します。

最後のループを次のように置き換えた場合:

for name, f in get_petters():
    print name + ":", 
    f()

あなたは実際に得ます:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

6

これは以下に由来します

for i in range(2): 
    pass

print(i)  # prints 1

の値を繰り返した後i、最終値として遅延保存されます。

ジェネレーターとしては機能します(つまり、各値を順番に出力します)が、リストに変換するときはジェネレーターで実行されるためcagecage.animal)へのすべての呼び出しは猫を返します。


0

質問を単純化しましょう。定義:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

次に、質問と同様に、次のようになります。

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

しかし、list()最初のものを作成しない場合:

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

どうしたの?このわずかな違いが結果を完全に変えるのはなぜですか?


を見るとlist(get_petters())、メモリアドレスの変更から、3つの異なる関数が実際に生成されていることがわかります。

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

ただし、cellこれらの関数がバインドされているsを見てください。

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

両方のループで、cellオブジェクトは反復全体を通じて同じままです。ただし、予想どおり、str2番目のループではそれが参照する特定の情報が異なります。cellオブジェクトが参照するanimalときに作成された、get_petters()と呼ばれています。ただし、ジェネレーター関数の実行時に参照するオブジェクトをanimal変更しstrます。

最初のループでは、各反復中にすべてのを作成しますfが、ジェネレーターget_petters()が完全に使い果たされ、list関数のがすでに作成された後でのみ、それらを呼び出します。

2番目のループでは、各反復中にget_petters()ジェネレーターを一時停止し、f各一時停止後に呼び出します。したがって、animalジェネレータ関数が一時停止しているその瞬間の値を取得することになります。

@Claudiuが同様の質問に答えるように:

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

[編集者注:iに変更されましたanimal。]

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.