Variables locales dans les fonctions imbriquées

105

D'accord, supportez-moi là-dessus, je sais que ça va paraître horriblement alambiqué, mais s'il vous plaît, aidez-moi à comprendre ce qui se passe.

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

Donne:

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

Donc, en gros, pourquoi est-ce que je n'ai pas trois animaux différents? Le cage«packagé» n'est-il pas dans la portée locale de la fonction imbriquée? Sinon, comment un appel à la fonction imbriquée recherche-t-il les variables locales?

Je sais que rencontrer ce genre de problèmes signifie généralement que l'on «fait mal», mais j'aimerais comprendre ce qui se passe.

non
la source
1
Essayez for animal in ['cat', 'dog', 'cow']... Je suis sûr que quelqu'un viendra et vous expliquera cela - c'est un de ces pièges à Python :)
Jon Clements

Réponses:

114

La fonction imbriquée recherche les variables de la portée parent lorsqu'elle est exécutée, pas lorsqu'elle est définie.

Le corps de la fonction est compilé et les variables «libres» (non définies dans la fonction elle-même par affectation) sont vérifiées, puis liées en tant que cellules de fermeture à la fonction, le code utilisant un index pour référencer chaque cellule. pet_functiona donc une variable libre ( cage) qui est alors référencée via une cellule de fermeture, index 0. La fermeture elle-même pointe vers la variable locale cagedans la get_pettersfonction.

Lorsque vous appelez réellement la fonction, cette fermeture est ensuite utilisée pour examiner la valeur de cagedans la portée environnante au moment où vous appelez la fonction . C'est là que réside le problème. Au moment où vous appelez vos fonctions, la get_pettersfonction a déjà terminé de calculer ses résultats. La cagevariable locale à un moment donné au cours de cette exécution a été attribué chacun des 'cow', 'dog'et des 'cat'cordes, mais à la fin de la fonction, cagecontient cette dernière valeur 'cat'. Ainsi, lorsque vous appelez chacune des fonctions renvoyées dynamiquement, vous obtenez la valeur 'cat'imprimée.

La solution consiste à ne pas compter sur les fermetures. Vous pouvez utiliser une fonction partielle à la place, créer une nouvelle portée de fonction ou lier la variable en tant que valeur par défaut pour un paramètre de mot-clé .

  • Exemple de fonction partielle, utilisant 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)))
  • Création d'un nouvel exemple de portée:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Lier la variable en tant que valeur par défaut pour un paramètre de mot-clé:

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

Il n'est pas nécessaire de définir la scoped_cagefonction dans la boucle, la compilation n'a lieu qu'une seule fois, pas à chaque itération de la boucle.

Martijn Pieters
la source
1
Je me suis cogné la tête contre ce mur pendant 3 heures aujourd'hui sur un scénario de travail. Votre dernier point est très important et c'est la principale raison pour laquelle j'ai rencontré ce problème. J'ai des rappels avec des fermetures à gogo tout au long de mon code, mais essayer la même technique en boucle est ce qui m'a fait.
DrEsperanto
12

Je crois comprendre que la cage est recherchée dans l'espace de noms de la fonction parent lorsque la fonction pet_function produite est effectivement appelée, pas avant.

Alors quand tu fais

funs = list(get_petters())

Vous générez 3 fonctions qui trouveront la dernière cage créée.

Si vous remplacez votre dernière boucle par:

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

Vous obtiendrez en fait:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey
la source
6

Cela découle de ce qui suit

for i in range(2): 
    pass

print(i)  # prints 1

après l'itération, la valeur de iest stockée paresseusement comme valeur finale.

En tant que générateur, la fonction fonctionnerait (c'est-à-dire imprimer chaque valeur à son tour), mais lors de la transformation en une liste, elle s'exécute sur le générateur , donc tous les appels à cage( cage.animal) retournent des chats.

Andy Hayden
la source
0

Simplifions la question. Définir:

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

        yield (animal, pet_function)

Ensuite, comme dans la question, nous obtenons:

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

Mais si on évite de créer une list()première:

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

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

Que se passe-t-il? Pourquoi cette différence subtile change-t-elle complètement nos résultats?


Si nous regardons list(get_petters()), il est clair d'après les adresses mémoire changeantes que nous fournissons en effet trois fonctions différentes:

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

Cependant, regardez les cells auxquels ces fonctions sont liées:

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

Pour les deux boucles, l' cellobjet reste le même tout au long des itérations. Cependant, comme prévu, le spécifique auquel stril fait référence varie dans la deuxième boucle. L' cellobjet fait référence à animal, qui est créé lors de l' get_petters()appel. Cependant, animalchange l' strobjet auquel il fait référence lorsque la fonction de générateur s'exécute .

Dans la première boucle, à chaque itération, nous créons tous les fs, mais nous ne les appelons que lorsque le générateur get_petters()est complètement épuisé et qu'une listde fonctions est déjà créée.

Dans la deuxième boucle, à chaque itération, nous mettons le get_petters()générateur en pause et appelons faprès chaque pause. Ainsi, nous finissons par récupérer la valeur de animalà ce moment dans le temps où la fonction de générateur est mise en pause.

Comme @Claudiu répond à une question similaire :

Trois fonctions distinctes sont créées, mais elles ont chacune la fermeture de l'environnement dans lequel elles sont définies - dans ce cas, l'environnement global (ou l'environnement de la fonction externe si la boucle est placée à l'intérieur d'une autre fonction). C'est exactement le problème, cependant - dans cet environnement, il animalest muté, et les fermetures se réfèrent toutes au même animal.

[Note de l'éditeur: ia été remplacé par animal.]

Mateen Ulhaq
la source