Question

Alors que je recherchais un problème de fermeture lexicale 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 délibérément 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 * 

Alors que je recherchais un problème de fermeture lexicale 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 délibérément lambda . Il imprime "4 4 4", ce qui est surprenant. Je m'attendrais à "0 2 4".

Ce code Perl équivalent le fait correctement:

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)

"0 2 4" est imprimé.

Pouvez-vous expliquer la différence?

Mise à jour:

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

<*>

Comme le montre la ligne commentée, i est inconnu à ce stade. Néanmoins, il affiche "4 4 4".

[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 : i est global. Cela affiche le même comportement:

<*>

Comme le montre la ligne commentée, i est inconnu à ce stade. Néanmoins, il affiche "4 4 4".

Était-ce utile?

La solution

Python se comporte réellement tel que défini. Trois fonctions distinctes sont créées, mais elles ont chacune la fermeture de l'environnement dans lequel elles ont été définies - dans ce cas, l'environnement global (ou l'environnement de la fonction externe si la boucle est placée dans une autre fonction). C’est exactement le problème - dans cet environnement, i est muté et les fermetures font référence au même i .

Voici la meilleure solution que je puisse trouver: créez un créateur de fonction et appelez that à la place. Cela forcera différents environnements pour chacune des fonctions créées, avec un i différent dans chacune d'elles.

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

Autres conseils

Les fonctions définies dans la boucle continuent à accéder à la même variable i tant que sa valeur change. À la fin de la boucle, toutes les fonctions pointent sur la même variable, qui contient la dernière valeur de la boucle: l’effet est celui indiqué dans l’exemple.

Pour évaluer i et utiliser sa valeur, un modèle courant consiste à 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 ainsi la valeur de la variable de boucle est gelée.

Ce qui suit 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)

Voici comment procéder à l'aide de 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.

regardez ceci:

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 la valeur 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))

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 où elle est appelée. 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 concerne 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 dans les autres langages. Si vous utilisez une variable différente créée de manière externe à la boucle et que vous la mutez, le même comportement apparaît 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))
))

Regardez ici pour en savoir plus.

[Modifier] Une meilleure façon de la décrire consiste peut-être à considérer la boucle do comme une macro exécutant les étapes suivantes:

  1. Définissez 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 en tant que 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 parente mais une toute nouvelle variable dans sa propre portée (c'est-à-dire le paramètre de la lambda) et vous obtenez ainsi le comportement que vous observez. Python n’ayant pas cette nouvelle portée implicite, le corps de la boucle for ne fait que partager la variable i.

Je ne suis toujours pas totalement convaincu pourquoi, dans certaines langues, cela fonctionne dans un sens ou dans un autre. 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)))

Imprimés "6 6 6" (notez qu'ici la liste va de 1 à 3, et a été construite à l'envers). 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)

Impressions "6 4 2"

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

Le problème est que toutes les fonctions locales sont liées 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

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 problème, mais bien le fait qu'il s'agisse d'une variable. à partir 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 provient du domaine de la fonction kkk et rien ne change à cela lorsque vous appelez les fonctions sur flist .

Le raisonnement derrière le comportement a déjà été expliqué et de nombreuses solutions ont été publiées, mais je pense que c'est le plus pythonique (rappelez-vous que tout ce qui est 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, elle utilise un générateur de fonctions, mais la réponse de piro est un bidouillage, pour être honnête, car cela fait de moi un "caché". argument avec une valeur par défaut (cela fonctionnera bien, mais ce n'est pas "pythonique").

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top