110

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

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ネストされた関数のローカルスコープに「パッケージ化」されていませんか?そうでない場合、ネストされた関数の呼び出しはどのようにローカル変数を検索しますか?

この種の問題に遭遇することは、通常、「間違ったことをしている」ことを意味することを私は知っていますが、何が起こるかを理解したいと思います。

4

4 に答える 4

119

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

関数本体がコンパイルされ、「自由」変数(割り当てによって関数自体に定義されていない)が検証され、インデックスを使用して各セルを参照するコードを使用して、クロージャーセルとして関数にバインドされます。pet_functionしたがって、1つの自由変数( )があり、これはクロージャセル(インデックス0)を介して参照されます。クロージャ自体は、関数内cageのローカル変数を指します。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回だけ行われます。

于 2012-09-14T11:37:39.570 に答える
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.
于 2012-09-14T11:39:16.727 に答える
7

これは以下に由来します

for i in range(2): 
    pass

print(i)  # prints 1

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

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

于 2012-09-14T11:38:19.917 に答える
1

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

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これらの関数がバインドされているを見てください。

>>> 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オブジェクトは反復全体を通じて同じままです。ただし、予想どおり、str参照する具体的な内容は2番目のループで異なります。cellオブジェクトは、が呼び出さanimalれたときに作成されるを参照します。get_petters()ただし、ジェネレーター関数の実行時に参照するオブジェクトをanimal変更します。str

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

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

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

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

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

于 2020-01-20T16:02:51.193 に答える