158 votes

Fermetures lexicales en Python

Alors que j'enquêtais sur un problème que j'avais avec fermetures lexicales dans le code Javascript, je suis venu le long de ce problème en Python:

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

Notez que cet exemple en pleine conscience évite lambda. Il imprime "4 4 4", ce qui est surprenant. Je m'attends à "0 2 4".

C'est l'équivalent du code Perl-t-il droit:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

"0 2 4" est imprimé.

Pouvez-vous nous expliquer la différence ?


Mise à jour:

Le problème n'est pas avec i mondial. Cela affiche le même comportement:

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)

Comme le commentaire de la ligne, i est inconnu à ce point. Pourtant, il imprime "4 4 4".

159voto

Claudiu Points 58398

Python se comporte vraiment comme définis. Trois fonctions distinctes sont créées, mais ils ont chacun la clôture de l'environnement qu'ils sont définis dans - dans ce cas, l'environnement mondial (ou à l'extérieur de la fonction de l'environnement si la boucle est placée à l'intérieur d'une autre fonction). C'est exactement le problème, cependant, dans cet environnement, je est muté, et de la fermeture de tous référence à la même chose, j'.

Ici est la meilleure solution je peux trouver - créer une fonction de créateur et d'invoquer que la place. Ce sera la force des environnements différents pour chacune des fonctions créées, avec un différents dans chacun d'eux.

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)

C'est ce qui arrive quand on mélange des effets secondaires et de la programmation fonctionnelle.

158voto

piro Points 4848

Les fonctions définies dans la boucle de garder l'accès à la même variable i alors que sa valeur change. À la fin de la boucle, toutes les fonctions de point à la même variable, qui est la dernière valeur dans la boucle: l'effet est ce qui se rapporte à l'exemple.

Afin d'évaluer i et l'utilisation de sa valeur, un modèle commun est de le définir comme un paramètre par défaut: valeurs par défaut des paramètres sont évalués lors de la def instruction est exécutée, et donc la valeur de la variable de boucle est gelé.

Les ouvrages suivants, comme prévu:

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)

36voto

Luca Invernizzi Points 1890

Voici comment vous le faites en utilisant la bibliothèque functools (dont je ne suis pas certain qu'elle était disponible au moment où la question a été posée).

 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)
 

Sorties 0 2 4, comme prévu.

14voto

Null303 Points 785

regarde ça:

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

Cela signifie qu'ils pointent tous vers la même instance de variable i, qui aura une valeur de 2 une fois la boucle terminée.

Une solution lisible:

 for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))
 

8voto

Brian Points 48423

Ce qui se passe est que la variable i est capturé, et les fonctions retournent la valeur, il est lié à la fois, il est appelé. Dans les langages fonctionnels ce genre de situation ne surgit jamais, tant que je ne serais pas de rebond. Cependant, avec python, et aussi comme vous l'avez vu avec lisp, ce n'est plus vrai.

La différence avec votre régime d'exemple est à voir avec la sémantique de la boucle. Le schéma est effectivement la création d'une nouvelle variable i à chaque fois dans la boucle, plutôt que de la réutilisation de l'existant, j'en liaison avec les autres langues. Si vous utilisez une autre variable externe de la boucle et de les transformer, vous verrez le même comportement dans le schéma. Essayez de remplacer votre boucle avec:

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

Jetez un oeil ici pour une discussion plus approfondie de cette.

[Edit] Probablement une meilleure façon de le décrire est de penser à faire une boucle comme une macro qui effectue les étapes suivantes:

  1. Définir un lambda qui prend un seul paramètre (i), avec un corps défini par le corps de la boucle,
  2. Un appel immédiat de la lambda avec les valeurs appropriées de je comme paramètre.

c'est à dire. l'équivalent de l'python ci-dessous:

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

Le je n'est plus celle de la portée parent, mais une marque nouvelle variable dans son propre champ (ie. le paramètre de la lambda) et ainsi vous obtenez le comportement à observer. Python n'a pas l'implicite nouveau champ d'application, de sorte que le corps de la boucle for seulement les actions de la variable i.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X