Créer des fonctions dans une boucle

102

J'essaye de créer des fonctions à l'intérieur d'une boucle:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

Le problème est que toutes les fonctions finissent par être les mêmes. Au lieu de renvoyer 0, 1 et 2, les trois fonctions renvoient 2:

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

Pourquoi cela se produit-il et que dois-je faire pour obtenir 3 fonctions différentes qui produisent respectivement 0, 1 et 2?

Sharvey
la source
4
pour me souvenir
...

Réponses:

167

Vous rencontrez un problème de liaison tardive - chaque fonction recherche le iplus tard possible (ainsi, lorsqu'elle est appelée après la fin de la boucle, ielle sera définie sur 2).

Facilement corrigé en forçant la liaison anticipée: changez def f():pour def f(i=i):ceci:

def f(i=i):
    return i

Les valeurs par défaut (la droite idans i=iune valeur par défaut pour le nom de l' argument i, qui est la main gauche ien i=i) sont cherchés à deftemps, pas le calltemps, donc essentiellement ils sont un moyen de recherche spécifiquement pour la liaison précoce.

Si vous craignez d' fobtenir un argument supplémentaire (et donc potentiellement d'être appelé à tort), il existe une manière plus sophistiquée qui impliquait d'utiliser une fermeture comme une "fabrique de fonctions":

def make_f(i):
    def f():
        return i
    return f

et dans votre boucle, utilisez f = make_f(i)plutôt que l' definstruction.

Alex Martelli
la source
7
comment savez-vous comment résoudre ces problèmes?
alwbtc
3
@alwbtc c'est surtout de l'expérience, la plupart des gens ont fait face à ces choses par eux-mêmes à un moment donné.
ruohola
Pouvez-vous expliquer pourquoi cela fonctionne s'il vous plaît? (Vous me sauvez sur le callback généré en boucle, les arguments étaient toujours les derniers de la boucle alors merci!)
Vincent Bénet
20

L'explication

Le problème ici est que la valeur de in'est pas enregistrée lorsque la fonction fest créée. Au contraire, frecherche la valeur du imoment où il est appelé .

Si vous y réfléchissez, ce comportement est parfaitement logique. En fait, c'est le seul moyen raisonnable pour les fonctions de fonctionner. Imaginez que vous ayez une fonction qui accède à une variable globale, comme ceci:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Lorsque vous lisez ce code, vous vous attendez - bien sûr - à ce qu'il affiche "bar", pas "foo", car la valeur de global_vara changé après la déclaration de la fonction. La même chose se produit dans votre propre code: au moment où vous appelez f, la valeur de ia changé et a été définie sur 2.

La solution

Il existe en fait de nombreuses façons de résoudre ce problème. Voici quelques options:

  • Forcer la liaison anticipée de ien l'utilisant comme argument par défaut

    Contrairement aux variables de fermeture (comme i), les arguments par défaut sont évalués immédiatement lorsque la fonction est définie:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)

    Pour donner un petit aperçu de comment / pourquoi cela fonctionne: Les arguments par défaut d'une fonction sont stockés en tant qu'attribut de la fonction; ainsi la valeur actuelle de iest instantané et sauvegardée.

    >>> i = 0
    >>> def f(i=i):
    ...     pass
    >>> f.__defaults__  # this is where the current value of i is stored
    (0,)
    >>> # assigning a new value to i has no effect on the function's default arguments
    >>> i = 5
    >>> f.__defaults__
    (0,)
  • Utiliser une fabrique de fonctions pour capturer la valeur actuelle de idans une fermeture

    La racine de votre problème est qu'il is'agit d'une variable qui peut changer. Nous pouvons contourner ce problème en créant une autre variable qui est garantie de ne jamais changer - et le moyen le plus simple de le faire est une fermeture :

    def f_factory(i):
        def f():
            return i  # i is now a *local* variable of f_factory and can't ever change
        return f
    
    for i in range(3):           
        f = f_factory(i)
        functions.append(f)
  • Utilisez functools.partialpour lier la valeur actuelle de iàf

    functools.partialvous permet d'attacher des arguments à une fonction existante. D'une certaine manière, c'est aussi une sorte d'usine de fonctions.

    import functools
    
    def f(i):
        return i
    
    for i in range(3):    
        f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
        functions.append(f_with_i)

Attention: ces solutions ne fonctionnent que si vous attribuez une nouvelle valeur à la variable. Si vous modifiez l'objet stocké dans la variable, vous rencontrerez à nouveau le même problème:

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

Remarquez à quel point il a iencore changé même si nous l'avons transformé en argument par défaut! Si votre code mute i , vous devez lier une copie de ià votre fonction, comme ceci:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
Aran-Fey
la source