Récemment, j'ai commencé à jouer avec Python et j'ai trouvé quelque chose de particulier dans le fonctionnement des fermetures. Considérez le code suivant:
adders=[0,1,2,3]
for i in [0,1,2,3]:
adders[i]=lambda a: i+a
print adders[1](3)
Il crée un tableau simple de fonctions qui prennent une seule entrée et retournent cette entrée ajoutée par un nombre. Les fonctions sont construites en for
boucle où l'itérateur i
va de 0
à 3
. Pour chacun de ces nombres, une lambda
fonction est créée qui la capture i
et l'ajoute à l'entrée de la fonction. La dernière ligne appelle la deuxième lambda
fonction avec 3
comme paramètre. À ma grande surprise, la sortie était 6
.
Je m'attendais à un 4
. Mon raisonnement était: en Python, tout est un objet et donc chaque variable est un pointeur essentiel pour lui. Lors de la création des lambda
fermetures pour i
, je m'attendais à ce qu'il stocke un pointeur sur l'objet entier actuellement pointé par i
. Cela signifie que lorsqu'il est i
affecté à un nouvel objet entier, il ne doit pas affecter les fermetures précédemment créées. Malheureusement, l'inspection du adders
tableau dans un débogueur montre que c'est le cas. Toutes les lambda
fonctions se rapportent à la dernière valeur i
, 3
dont les résultats en adders[1](3)
retour 6
.
Ce qui me fait me demander ce qui suit:
- Que capturent exactement les fermetures?
- Quelle est la façon la plus élégante de convaincre les
lambda
fonctions de capturer la valeur actuelle d'i
une manière qui ne sera pas affectée lors d'uni
changement de valeur?
i
quitte l'espace de noms?print i
cela ne fonctionnerait pas après la boucle. Mais je l'ai testé par moi-même et maintenant je vois ce que vous voulez dire - cela fonctionne. Je n'avais aucune idée que les variables de boucle persistaient après le corps de la boucle en python.if
,with
,try
etc.Réponses:
On a répondu à votre deuxième question, mais comme pour la première:
La portée en Python est
dynamique etlexicale. Une fermeture se souviendra toujours du nom et de la portée de la variable, pas de l'objet vers lequel elle pointe. Étant donné que toutes les fonctions de votre exemple sont créées dans la même étendue et utilisent le même nom de variable, elles se réfèrent toujours à la même variable.EDIT: Concernant votre autre question sur la façon de surmonter cela, il y a deux façons qui me viennent à l'esprit:
La manière la plus concise, mais pas strictement équivalente, est celle recommandée par Adrien Plisson . Créez un lambda avec un argument supplémentaire et définissez la valeur par défaut de l'argument supplémentaire sur l'objet que vous souhaitez conserver.
Un peu plus verbeux mais moins hacky serait de créer une nouvelle portée à chaque fois que vous créez le lambda:
La portée ici est créée à l'aide d'une nouvelle fonction (un lambda, par souci de concision), qui lie son argument et transmet la valeur que vous souhaitez lier comme argument. Dans le code réel, cependant, vous aurez très probablement une fonction ordinaire au lieu du lambda pour créer la nouvelle étendue:
la source
set!
. voir ici quelle est réellement l'étendue dynamique: voidspace.org.uk/python/articles/code_blocks.shtml .vous pouvez forcer la capture d'une variable en utilisant un argument avec une valeur par défaut:
l'idée est de déclarer un paramètre (intelligemment nommé
i
) et de lui donner une valeur par défaut de la variable que vous souhaitez capturer (la valeur dei
)la source
Pour être complet, une autre réponse à votre deuxième question: vous pouvez utiliser partial dans le module functools .
Avec l'importation d'ajout depuis l'opérateur comme Chris Lutz l'a proposé, l'exemple devient:
la source
Considérez le code suivant:
Je pense que la plupart des gens ne trouveront pas cela déroutant du tout. C'est le comportement attendu.
Alors, pourquoi les gens pensent-ils que ce serait différent quand cela se fait en boucle? Je sais que j'ai fait cette erreur moi-même, mais je ne sais pas pourquoi. C'est la boucle? Ou peut-être la lambda?
Après tout, la boucle n'est qu'une version plus courte de:
la source
i
variable est accessible pour chaque fonction lambda.En réponse à votre deuxième question, la façon la plus élégante de le faire serait d'utiliser une fonction qui prend deux paramètres au lieu d'un tableau:
Cependant, utiliser lambda ici est un peu idiot. Python nous donne le
operator
module, qui fournit une interface fonctionnelle aux opérateurs de base. Le lambda ci-dessus a une surcharge inutile juste pour appeler l'opérateur d'addition:Je comprends que vous jouez, essayez d'explorer le langage, mais je ne peux pas imaginer une situation où j'utiliserais un éventail de fonctions où l'étrangeté de la portée de Python entraverait.
Si vous le souhaitez, vous pouvez écrire une petite classe qui utilise votre syntaxe d'indexation de tableau:
la source
Voici un nouvel exemple qui met en évidence la structure des données et le contenu d'une fermeture, pour aider à clarifier quand le contexte englobant est «enregistré».
Qu'y a-t-il dans une fermeture?
Notamment, my_str n'est pas dans la fermeture de f1.
Qu'y a-t-il dans la fermeture de f2?
Notez (à partir des adresses mémoire) que les deux fermetures contiennent les mêmes objets. Vous pouvez donc commencer à penser à la fonction lambda comme ayant une référence à la portée. Cependant, my_str n'est pas dans la fermeture pour f_1 ou f_2, et i n'est pas dans la fermeture pour f_3 (non illustré), ce qui suggère que les objets de fermeture eux-mêmes sont des objets distincts.
Les objets de fermeture sont-ils le même objet?
la source
int object at [address X]>
m'a fait penser que la fermeture stocke [adresse X] AKA une référence. Cependant, [adresse X] changera si la variable est réaffectée après l'instruction lambda.