Creazione di funzioni in un ciclo


102

Sto cercando di creare funzioni all'interno di un ciclo:

functions = []

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

    # alternatively: f = lambda: i

    functions.append(f)

Il problema è che tutte le funzioni finiscono per essere le stesse. Invece di restituire 0, 1 e 2, tutte e tre le funzioni restituiscono 2:

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

Perché sta accadendo e cosa dovrei fare per ottenere 3 diverse funzioni che generano rispettivamente 0, 1 e 2?


Risposte:


167

Stai riscontrando un problema con l' associazione tardiva : ogni funzione cerca il ipiù tardi possibile (quindi, quando viene chiamata dopo la fine del ciclo,i sarà impostata su 2).

Facilmente risolto forzando l'associazione anticipata: cambia def f():in def f(i=i):questo modo:

def f(i=i):
    return i

I valori predefiniti (la mano destra iin i=iè un valore predefinito per il nome dell'argomento i, che è la mano sinistra iin i=i) vengono cercatidef volta volta, non di callvolta, quindi essenzialmente sono un modo per cercare specificamente l'associazione anticipata.

Se sei preoccupato di fottenere un argomento in più (e quindi potenzialmente essere chiamato in modo errato), c'è un modo più sofisticato che prevedeva l'utilizzo di una chiusura come "fabbrica di funzioni":

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

e nel tuo ciclo usa f = make_f(i)invece defdell'istruzione.


7
come fai a sapere come riparare queste cose?
alwbtc

3
@alwbtc è per lo più solo esperienza, la maggior parte delle persone ha affrontato queste cose da sola ad un certo punto.
ruohola

Puoi spiegare perché funziona per favore? (Mi salvi sulla richiamata generata in loop, gli argomenti erano sempre gli
ultimi

20

La spiegazione

Il problema qui è che il valore di inon viene salvato quando fviene creata la funzione . Piuttosto, fcerca il valore di iquando viene chiamato .

Se ci pensi, questo comportamento ha perfettamente senso. In effetti, è l'unico modo ragionevole in cui le funzioni possono funzionare. Immagina di avere una funzione che accede a una variabile globale, come questa:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Quando leggete questo codice, vi aspettereste - ovviamente - che stampi "bar", non "foo", perché il valore di global_varè cambiato dopo che la funzione è stata dichiarata. La stessa cosa sta accadendo nel tuo codice: nel momento in cui chiami f, il valore di iè cambiato ed è stato impostato su2 .

La soluzione

In realtà ci sono molti modi per risolvere questo problema. Ecco alcune opzioni:

  • Forza l'associazione anticipata di i utilizzandolo come argomento predefinito

    A differenza delle variabili di chiusura (come i), gli argomenti predefiniti vengono valutati immediatamente quando la funzione viene definita:

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

    Per dare un po 'di intuizione su come / perché funziona: Gli argomenti predefiniti di una funzione sono memorizzati come attributo della funzione; in questo modo il valore corrente di iviene acquisito e salvato.

    >>> 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,)
  • Utilizzare una factory di funzioni per acquisire il valore corrente di iin una chiusura

    La radice del tuo problema è che iè una variabile che può cambiare. Possiamo aggirare questo problema creando un'altra variabile che è garantito che non cambierà mai e il modo più semplice per farlo è una chiusura :

    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)
  • Utilizzare functools.partialper associare il valore corrente di iaf

    functools.partialconsente di allegare argomenti a una funzione esistente. In un certo senso, anch'essa è una specie di fabbrica di funzioni.

    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)

Avvertenza: queste soluzioni funzionano solo se si assegna un nuovo valore alla variabile. Se modifichi l'oggetto memorizzato nella variabile, riscontrerai di nuovo lo stesso problema:

>>> 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]

Nota come è iancora cambiato anche se lo abbiamo trasformato in un argomento predefinito! Se il tuo codice muta i , devi associare una copia di ialla tua funzione, in questo modo:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.