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, i
est inconnu à ce stade. Pourtant, il imprime "4 4 4".
python
closures
lazy-evaluation
late-binding
Eli Bendersky
la source
la source
Réponses:
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.
C'est ce qui se passe lorsque vous mélangez effets secondaires et programmation fonctionnelle.
la source
def inner(x, i=i): return x * i
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 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 consiste à le définir comme paramètre par défaut: les valeurs par défaut des paramètres sont évaluées lorsque l'def
instruction est exécutée, et ainsi la valeur de la variable de boucle est gelée.Les travaux suivants fonctionnent comme prévu:
la source
def
instruction est exécutée /i
de la définition. :-(Voici comment procéder en utilisant la
functools
bibliothèque (dont je ne suis pas sûr qu'elle était disponible au moment où la question a été posée).Sorties 0 2 4, comme prévu.
la source
functools.partialmethod()
partir de python 3.4regarde ça:
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:
la source
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:
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:
c'est à dire. l'équivalent du python ci-dessous:
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.
la source
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:
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:
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.
la source
Le problème est que toutes les fonctions locales se lient au même environnement et donc à la même
i
variable. La solution (contournement) consiste à créer des environnements séparés (cadres de pile) pour chaque fonction (ou lambda):la source
La variable
i
est un global, dont la valeur est 2 à chaque fois que la fonctionf
est appelée.Je serais enclin à mettre en œuvre le comportement que vous recherchez comme suit:
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 dei
est tirée de la portée de lakkk
fonction, et rien ne change cela lorsque vous appelez les fonctionsflist
.la source
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!):
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") .
la source
func
inx * func.i
fera 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.