web-dev-qa-db-fra.com

Que capturent les fermetures de fonctions (lambda)?

Récemment, j'ai commencé à jouer avec Python) et je suis arrivé à 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 construit un tableau simple de fonctions qui prennent une seule entrée et renvoient cette entrée ajoutée par un nombre. Les fonctions sont construites dans la boucle for où l'itérateur i va de 0 À 3. Pour chacun de ces nombres, une fonction lambda est créée. Elle capture i et l'ajoute à l'entrée de la fonction. La dernière ligne appelle la deuxième fonction lambda avec le paramètre 3. À ma grande surprise, le résultat était 6.

Je m'attendais à un 4. Mon raisonnement était le suivant: in Python tout est un objet et donc chaque variable est un pointeur essentiel sur celui-ci. Lors de la création des fermetures lambda pour i, Je m'attendais à ce qu'il stocke un pointeur sur l'objet entier actuellement pointé par i, ce qui signifie que lorsque i assignerait un nouvel objet entier, il ne devrait pas affecter les fermetures précédemment créées. , inspecter le tableau adders dans un débogueur montre que c’est le cas. Toutes les fonctions lambda font référence à la dernière valeur de i, 3, qui résulte en adders[1](3) retournant 6.

Ce qui me fait me poser des questions sur les points suivants:

  • Qu'est-ce que les fermetures capturent exactement?
  • Quel est le moyen le plus élégant de convaincre les fonctions lambda de capturer la valeur actuelle de i de manière à ne pas être affectée lorsque i change sa valeur?
225
Boaz

Votre deuxième question a reçu une réponse, mais en ce qui concerne votre première:

qu'est-ce que la fermeture capture exactement?

Portée dans Python est dynamique et lexical. Une fermeture se souviendra toujours du nom et de la portée de la variable, pas de l'objet pointé. Comme toutes les fonctions de votre exemple sont créées dans la même portée et utilisent le même nom de variable, elles font toujours référence à la même variable.

EDIT: En ce qui concerne votre autre question, à savoir comment surmonter ceci, il y a deux manières qui vous viennent à l'esprit:

  1. La manière la plus concise, mais pas strictement équivalente, est la recommandée par Adrien Plisson . Créez un lambda avec un argument supplémentaire et définissez la valeur par défaut de cet argument sur l'objet à préserver.

  2. Il serait un peu plus verbeux mais moins hacky de créer une nouvelle portée chaque fois que vous créez le lambda:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5
    

    La portée ici est créée en utilisant une nouvelle fonction (une lambda, par souci de brièveté), qui lie son argument et en transmettant la valeur que vous voulez lier comme argument. En code réel, vous aurez probablement une fonction ordinaire à la place du lambda pour créer la nouvelle portée:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    
144
Max Shawabkeh

vous pouvez forcer la capture d'une variable en utilisant un argument avec une valeur par défaut:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

l'idée est de déclarer un paramètre (astucieusement nommé i) et de lui donner une valeur par défaut de la variable que vous souhaitez capturer (la valeur de i)

176
Adrien Plisson

Pour être complet, une autre réponse à votre deuxième question: vous pourriez utiliser partiel dans le module functools .

Avec l’importation d’ajout à partir d’opérateur, comme Chris Lutz l’a proposé, l’exemple devient:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)
29
Joma

Considérons le code suivant:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

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 je me suis trompé moi-même, mais je ne sais pas pourquoi. C'est la boucle? Ou peut-être le lambda?

Après tout, la boucle est juste une version plus courte de:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a
20
truppo

Voici un nouvel exemple qui met en évidence la structure de données et le contenu d'une fermeture, pour aider à préciser quand le contexte englobant est "enregistré".

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Qu'y a-t-il dans une fermeture?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Notamment, my_str n'est pas dans la fermeture de f1.

Qu'y a-t-il dans la fermeture de f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Notez (à partir des adresses de mémoire) que les deux fermetures contiennent les mêmes objets. Donc, vous pouvez commencer à considérer la fonction lambda comme 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 représentée), ce qui suggère que les objets de fermeture eux-mêmes sont des objets distincts.

Les objets de fermeture eux-mêmes sont-ils le même objet?

>>> print f_1.func_closure is f_2.func_closure
False
3
Jeff

En réponse à votre deuxième question, la manière la plus élégante de le faire serait d’utiliser une fonction qui prend deux paramètres au lieu d’un tableau:

add = lambda a, b: a + b
add(1, 3)

Cependant, utiliser lambda est un peu ridicule. Python nous donne le module operator, qui fournit une interface fonctionnelle aux opérateurs de base. Le lambda ci-dessus impose une surcharge inutile simplement pour appeler l'opérateur d'addition:

from operator import add
add(1, 3)

Je comprends que vous jouez, que vous essayez d’explorer la langue, mais je ne peux pas imaginer une situation où j’utiliserais un tableau de fonctions où l’étrange étrangeté de Python gênerait.

Si vous le souhaitez, vous pouvez écrire une petite classe qui utilise votre syntaxe d'indexation de tableau:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)
3
Chris Lutz