Comment fonctionnent les fermetures lexicales?

pendant que j'enquêtais sur un problème que j'avais avec les fermetures lexicales en 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 consciemment lambda . Il imprime "4 4 4", ce qui est surprenant. Je m'attendais à "0 2 4".

ce code Perl équivalent est correct:

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 expliquer la différence ?


mise à Jour:

le problème n'est pas avec i étant 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 est inconnu à ce point. Pourtant, il imprime "4 4 4".

133
demandé sur martineau 2008-10-24 18:08:25

9 réponses

Python se comporte comme défini. trois fonctions distinctes sont créées, mais elles ont chacune la fermeture de l'environnement elles sont définies dans - 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, bien que - dans cet environnement, I est muté , et les fermetures tous se réfèrent à la même i .

Voici la meilleure solution que je puisse trouver - Créer un créateur de fonction et invoquer que à la place. Cela va forcer différents environnements 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 se produit lorsque vous mélangez les effets secondaires et la programmation fonctionnelle.

141
répondu Claudiu 2011-06-17 21:53:06

les fonctions définies dans la boucle maintiennent l'accès à 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 retient la dernière valeur de la boucle: l'effet est ce qui est rapporté dans l'exemple.

afin d'évaluer i et d'utiliser sa valeur, un motif courant est de le Définir comme un paramètre par défaut: les paramètres par défaut sont évalués lorsque la déclaration def est exécutée, et ainsi, la valeur de la variable de boucle est gelé.

fonctionne 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)
149
répondu piro 2008-10-26 12:48:27

Voici comment utiliser la bibliothèque functools (qui n'était pas 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.

26
répondu Luca Invernizzi 2017-10-17 16:40:57

regardez ç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 la 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))
13
répondu Null303 2008-10-24 14:52:15

ce qui se passe est que la variable i est capturée, et les fonctions renvoient la valeur à laquelle elle est liée au moment où elle est appelée. Dans les langues fonctionnelles, ce genre de situation ne se présente jamais, car je ne serais pas en train de rebondir. 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. Scheme crée effectivement une nouvelle variable i à chaque fois à travers la boucle, plutôt que de réutiliser l'existant, j'en liaison avec les autres langues. Si vous utilisez une variable différente créée à l'extérieur de la boucle et que vous la mutez, vous verrez le même comportement dans scheme. Essayez de remplacer votre boucle par:

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

regardez ici pour en savoir plus.

[Modifier] 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 en prenant un seul paramètre (i), avec un corps défini par le corps de la boucle,
  2. un Appel immédiat de cette lambda avec des valeurs appropriées de i comme paramètre.

ie. l'équivalent du 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 moi n'est plus celle de la portée parent, mais une marque nouvelle variable dans son propre champ (ie. le paramètre à la lambda) et ainsi vous obtenez le comportement à observer. Python n'a pas cette nouvelle portée implicite, donc le corps de la boucle for partage juste la variable I.

7
répondu Brian 2008-10-25 22:00:50

Je ne suis toujours pas entièrement convaincu pourquoi dans certaines langues cela fonctionne d'une manière, et d'une autre. En Lisp commun, 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 la liste est ici de 1 à 3, et construite à l'envers). Alors que dans Scheme il 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)

Imprime "6 4 2"

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

4
répondu Eli Bendersky 2008-10-25 07:27:49

le problème est que toutes les fonctions locales se lient au même environnement et donc à la même variable i . La solution (solution de contournement) est de créer des environnements séparés (cadres de pile) pour chaque fonction (ou lambda):

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4
2
répondu Rafał Dowgird 2008-10-24 14:42:55

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

je serais enclin à mettre en œuvre le comportement que vous cherchez 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 per se qui est à l'origine de ce comportement, c'est le fait qu'il s'agit d'une variable d'une portée enveloppante qui a un valeur fixe au cours des temps où f est appelé. Dans votre second exemple, la valeur de i est extraite de la portée de la fonction kkk , et rien ne change cela lorsque vous appelez les fonctions sur flist .

1
répondu Alex Coventry 2008-10-24 15:25:03

le raisonnement derrière le comportement a déjà été expliqué, et plusieurs solutions ont été publiées, mais je pense que c'est le plus pythonique (rappelez-vous, tout en Python est un objet!):

flist = []

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

for f in flist:
    print f(2)

la réponse de Claudiu est assez bonne, en utilisant un générateur de fonction, mais la réponse de piro est un hack, pour être honnête, car elle fait de moi un argument "caché" avec une valeur par défaut (ça marchera très bien, mais ce n'est pas "pythonic").

0
répondu darkfeline 2012-07-24 08:20:04