[python] Comment fonctionnent les fermetures lexicales?


Answers

Les fonctions définies dans la boucle continuent d'accéder à la même variable i pendant que sa valeur change. A la fin de la boucle, toutes les fonctions pointent vers la même variable, qui contient la dernière valeur dans la boucle: l'effet est ce qui est rapporté dans l'exemple.

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

Les travaux 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)
Question

Alors que j'étudiais un problème que j'avais avec les fermetures lexicales dans le code Javascript, j'ai rencontré 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 évite sciemment lambda . Il imprime "4 4 4", ce qui est surprenant. Je m'attendrais à "0 2 4".

Ce code Perl équivalent le fait bien:

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 s'il vous plaît expliquer la différence?

Mettre à jour:

Le problème n'est pas d'être global. 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 montre la ligne commentée, i inconnu à ce moment-là. Pourtant, il imprime "4 4 4".




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



La variable i est un global dont la valeur est 2 à chaque fois que la fonction f est appelée.

Je serais enclin à implémenter le comportement que vous recherchez comme suit:

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

Réponse à votre mise à jour : Ce n'est pas la globalité de i soi qui cause ce comportement, c'est le fait que c'est une variable d'une portée englobante qui a une valeur fixe sur les temps où f est appelée. Dans votre deuxième exemple, la valeur de i est tirée de la portée de la fonction kkk , et rien ne change lorsque vous appelez les fonctions sur flist .




Je ne suis toujours pas entièrement convaincu pourquoi dans certaines langues cela fonctionne d'une manière, et d'une autre manière. En Common Lisp, c'est comme Python:

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist* 
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)  
  (format t "~a~%" (funcall f 2)))

Imprime "6 6 6" (notez que ici la liste est de 1 à 3, et construit en inverse "). Dans Scheme cela fonctionne comme dans 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)

Impressions "6 4 2"

Et comme je l'ai déjà mentionné, Javascript est dans le camp Python / CL. Il semble qu'il y ait ici une décision de mise en œuvre, que différentes langues abordent de différentes manières. J'aimerais comprendre quelle est la décision, exactement.




Links