web-dev-qa-db-fra.com

Comment fonctionnent les fermetures lexicales?

Pendant 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 consciemment lambda. Il imprime "4 4 4", ce qui est surprenant. Je m'attendrais à "0 2 4".

Ce code Perl équivalent le fait correctement:

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 stade. Pourtant, il imprime "4 4 4".

143
Eli Bendersky

Python se comporte réellement comme défini. Trois fonctions distinctes sont créés, mais ils ont chacun la la fermeture de l'environnement dans lequel ils sont définis - 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, je suis mutéet toutes les fermetures se référer au même i.

Voici la meilleure solution que je peux trouver - créer un créateur de fonctions et invoquer cette au lieu. Cela forcera environnements différents pour chacune des fonctions créées, avec un différent je dans chacun.

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 des effets secondaires et une programmation fonctionnelle.

148
Claudiu

Les fonctions définies dans la boucle continuent d'accéder à la même variable i pendant que sa valeur change. À 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 i et d'utiliser sa valeur, un modèle courant 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 boucle variable est figé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)
153
piro

Voici comment vous le faites en utilisant la bibliothèque functools (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.

31
Luca Invernizzi

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))
13
Null303

Ce qui se passe, c'est que la variable i est capturée et que les fonctions renvoient la valeur à laquelle elle est liée au moment de son appel. Dans les langages fonctionnels, ce genre de situation ne se produit jamais, car je ne serais pas rebondi. Cependant avec python, et 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. Le schéma 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 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 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 oeil 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 des 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 ainsi vous obtenez le comportement que vous observez. Python n'a pas cette nouvelle portée implicite, donc le corps de la boucle for partage juste la variable i.

7
Brian

Je ne suis toujours pas entièrement convaincu pourquoi, dans certaines langues, cela fonctionne d'une manière et d'une autre. Dans 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 en sens inverse "). 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'aimerais comprendre quelle est la décision, exactement.

4
Eli Bendersky

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) consiste à créer des environnements distincts (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
Rafał Dowgird

La variable i est une variable globale, dont la valeur est 2 à chaque appel de la fonction f.

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 en soi qui est à l'origine de ce comportement, c'est le fait qu'il s'agit d'une variable d'une portée englobante qui a une valeur fixe sur les moments où f est appelé. Dans votre deuxième exemple, la valeur de i est prise dans la portée de la fonction kkk, et rien ne change cela lorsque vous appelez les fonctions sur flist.

1
Alex Coventry

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 Pythonic (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 fonctions, mais la réponse de piro est un hack, pour être honnête, car cela fait de i un argument "caché" avec une valeur par défaut (cela fonctionnera bien, mais ce n'est pas "Pythonic") .

0
darkfeline