Comment fonctionnent les fermetures lexicales?

149

Alors que j'étudiais un problème que j'avais avec les fermetures lexicales dans le code Javascript, je suis tombé sur 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'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 expliquer la différence?


Mettre à jour:

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

Eli Bendersky
la source

Réponses:

151

Python se comporte en fait comme défini. 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, i est muté , et les fermetures se réfèrent toutes au même i .

Voici la meilleure solution que je peux venir avec - créer un creater de fonction et invoquer ce lieu. Cela forcera des environnements différents pour chacune des fonctions créées, avec un i différent dans chacune.

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 passe lorsque vous mélangez effets secondaires et programmation fonctionnelle.

Claudiu
la source
5
Votre solution est également celle utilisée en Javascript.
Eli Bendersky
9
Ce n'est pas une mauvaise conduite. Il se comporte exactement comme défini.
Alex Coventry
6
IMO piro a une meilleure solution stackoverflow.com/questions/233673/…
jfs
2
Je changerais peut-être le «i» le plus profond en «j» pour plus de clarté.
eggyntax
7
def inner(x, i=i): return x * i
pourquoi
152

Les fonctions définies dans la boucle continuent d'accéder à la même variable ipendant 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 de la boucle: l'effet est ce qui est rapporté dans l'exemple.

Afin d'évaluer iet d'utiliser sa valeur, un modèle courant consiste à le définir comme paramètre par défaut: les valeurs par défaut des paramètres sont évaluées lorsque l' definstruction est exécutée, et ainsi la valeur de la variable de boucle est gelée.

Les travaux suivants fonctionnent 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)
piro
la source
7
s / au moment de la compilation / au moment où l' definstruction est exécutée /
jfs
23
C'est une solution ingénieuse, ce qui la rend horrible.
Stavros Korokithakis
Il y a un problème avec cette solution: func a maintenant deux paramètres. Cela signifie que cela ne fonctionne pas avec une quantité variable de paramètres. Pire encore, si vous appelez func avec un second paramètre, cela écrasera l'original ide la définition. :-(
Pascal
34

Voici comment procéder en utilisant la functoolsbibliothèque (dont je ne suis pas sûr 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.

Luca Invernizzi
la source
Je voulais vraiment l'utiliser mais ma fonction est en fait une méthode de classe et la première valeur transmise est self. Y a-t-il de toute façon autour de cela?
Michael David Watson
1
Absolument. Supposons que vous ayez une classe Math avec une méthode add (self, a, b), et que vous vouliez définir a = 1 pour créer la méthode 'incrément'. Ensuite, créez une instance de votre classe 'my_math', et votre méthode d'incrémentation sera 'increment = partial (my_math.add, 1)'.
Luca Invernizzi
2
Pour appliquer cette technique à une méthode que vous pouvez également utiliser à functools.partialmethod()partir de python 3.4
Matt Eding
13

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))
Null303
la source
1
Ma question est plus "générale". Pourquoi Python a-t-il cette faille? Je m'attendrais à ce qu'un langage supportant les fermetures lexicales (comme Perl et toute la dynastie Lisp) fonctionne correctement.
Eli Bendersky
2
Demander pourquoi quelque chose a un défaut suppose que ce n'est pas un défaut.
Null303
7

Ce qui se passe, c'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 langages fonctionnels, ce genre de situation ne se pose jamais, car je ne serais pas rebondi. Cependant avec python, et aussi comme vous l'avez vu avec lisp, ce n'est plus vrai.

La différence avec votre exemple de schéma est liée à la sémantique de la boucle do. Scheme crée effectivement une nouvelle variable i à chaque fois dans la boucle, plutôt que de réutiliser une liaison i existante comme avec les autres langages. Si vous utilisez une variable différente créée à l'extérieur de la boucle et que vous la mute, vous verrez le même comportement dans le schéma. 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))
))

Jetez un œil ici pour une discussion plus approfondie à ce sujet.

[Modifier] Peut-être une meilleure façon de le décrire est de considérer la boucle do comme une macro qui effectue les étapes suivantes:

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

c'est à dire. 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 i n'est plus celui de la portée parent mais une toute nouvelle variable dans sa propre portée (c'est-à-dire le paramètre du lambda) et vous obtenez ainsi le comportement que vous observez. Python n'a pas cette nouvelle portée implicite, donc le corps de la boucle for ne partage que la variable i.

Brian
la source
Intéressant. Je n'étais pas conscient de la différence de sémantique de la boucle do. Merci
Eli Bendersky
4

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 qu'ici la liste est de 1 à 3, et construite à l'envers "). Alors que dans Scheme, cela fonctionne comme en 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 ait une décision de mise en œuvre ici, que différentes langues abordent de manière distincte. J'adorerais comprendre quelle est la décision, exactement.

Eli Bendersky
la source
8
La différence réside dans le (faire ...) plutôt que dans les règles de cadrage. Dans le schéma, do crée une nouvelle variable à chaque passage dans la boucle, tandis que d'autres langages réutilisent la liaison existante. Voir ma réponse pour plus de détails et un exemple de version de schéma avec un comportement similaire à lisp / python.
Brian
2

Le problème est que toutes les fonctions locales se lient au même environnement et donc à la même ivariable. La solution (contournement) consiste à 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
Rafał Dowgird
la source
1

La variable iest un global, dont la valeur est 2 à chaque fois que la fonction fest appelée.

Je serais enclin à mettre en œuvre 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 per se 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 instants où f est appelé. Dans votre deuxième exemple, la valeur de iest tirée de la portée de la kkkfonction, et rien ne change cela lorsque vous appelez les fonctions flist.

Alex Coventry
la source
0

Le raisonnement derrière le comportement a déjà été expliqué et plusieurs solutions ont été publiées, mais je pense que c'est la 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 plutôt bonne, en utilisant un générateur de fonctions, mais la réponse de piro est un hack, pour être honnête, car cela transforme i en un argument "caché" avec une valeur par défaut (cela fonctionnera bien, mais ce n'est pas "pythonique") .

félin noir
la source
Je pense que cela dépend de votre version de python. Maintenant, je suis plus expérimenté et je ne suggérerais plus cette façon de faire. Claudiu est la bonne façon de faire une fermeture en Python.
darkfeline
1
Cela ne fonctionnera ni sur Python 2 ni sur 3 (ils affichent tous les deux "4 4 4"). Le funcin x * func.ifera toujours référence à la dernière fonction définie. Ainsi, même si chaque fonction a individuellement le numéro correct, elles finissent toutes par lire à partir de la dernière de toute façon.
Lambda Fairy