Creando funciones en un bucle

102

Estoy tratando de crear funciones dentro de un bucle:

functions = []

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

    # alternatively: f = lambda: i

    functions.append(f)

El problema es que todas las funciones terminan siendo iguales. En lugar de devolver 0, 1 y 2, las tres funciones devuelven 2:

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

¿Por qué sucede esto y qué debo hacer para obtener 3 funciones diferentes que generen 0, 1 y 2 respectivamente?

Sharvey
fuente
4
como un recordatorio para mí: docs.python-guide.org/en/latest/writing/gotchas/…
Chuntao Lu

Respuestas:

167

Tiene un problema con el enlace tardío : cada función busca lo más itarde posible (por lo tanto, cuando se llama después del final del ciclo, ise configurará en 2).

Se soluciona fácilmente forzando el enlace anticipado: cambie def f():a def f(i=i):esto:

def f(i=i):
    return i

Los valores por defecto (la mano derecha ien i=iun valor por defecto para el nombre de argumento i, que es el de la izquierda ien i=i) se buscan en defel tiempo, no en el calltiempo, lo que en esencia son una manera de buscar específicamente para la unión temprana.

Si está preocupado por fobtener un argumento adicional (y, por lo tanto, potencialmente ser llamado erróneamente), hay una forma más sofisticada que implica el uso de un cierre como una "fábrica de funciones":

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

y en su ciclo use en f = make_f(i)lugar de la defdeclaración.

Alex Martelli
fuente
7
¿Cómo sabes cómo solucionar estas cosas?
alwbtc
3
@alwbtc es principalmente una experiencia, la mayoría de las personas se han enfrentado a estas cosas por su cuenta en algún momento.
ruohola
¿Puede explicar por qué funciona, por favor? (Me salvas en la devolución de llamada generada en bucle, los argumentos siempre fueron los últimos del bucle, ¡así que gracias!)
Vincent Bénet
20

La explicación

El problema aquí es que el valor de ino se guarda cuando fse crea la función . Más bien, fbusca el valor de icuándo se llama .

Si lo piensas, este comportamiento tiene mucho sentido. De hecho, es la única forma razonable en que pueden funcionar las funciones. Imagina que tienes una función que accede a una variable global, como esta:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Cuando lea este código, por supuesto, esperaría que imprima "bar", no "foo", porque el valor de global_varha cambiado después de que se declaró la función. Lo mismo sucede en su propio código: en el momento en que llama f, el valor de iha cambiado y se ha establecido en 2.

La solución

En realidad, hay muchas formas de resolver este problema. Aquí hay algunas opciones:

  • Forzar la vinculación anticipada de iusándolo como argumento predeterminado

    A diferencia de las variables de cierre (como i), los argumentos predeterminados se evalúan inmediatamente cuando se define la función:

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

    Para dar una idea de cómo / por qué funciona esto: Los argumentos predeterminados de una función se almacenan como un atributo de la función; por lo tanto, se toma una instantánea y se guarda el valor actual de i.

    >>> 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,)
    
  • Utilice una función de fábrica para capturar el valor actual de ien un cierre

    La raíz de su problema es que ies una variable que puede cambiar. Podemos solucionar este problema creando otra variable que se garantiza que nunca cambiará, y la forma más fácil de hacerlo es un cierre :

    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)
    
  • Úselo functools.partialpara vincular el valor actual de iaf

    functools.partialle permite adjuntar argumentos a una función existente. En cierto modo, también es una especie de fábrica de funciones.

    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)
    

Advertencia: estas soluciones solo funcionan si asigna un nuevo valor a la variable. Si modifica el objeto almacenado en la variable, experimentará el mismo problema nuevamente:

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

¡Observe cómo iaún cambió a pesar de que lo convertimos en un argumento predeterminado! Si su código muta i , entonces debe vincular una copia de ia su función, así:

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