Creación de funciones (o lambdas) en un bucle (o comprensión)

7 minutos de lectura

avatar de usuario
sharvey

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 acaban 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?


Un problema como este es especialmente común en el código Tkinter (o para otros juegos de herramientas GUI), donde el objetivo es crear varios botones con funciones relacionadas (al tener cada uno un argumento diferente para la misma devolución de llamada). Consulte tkinter creando botones en los argumentos de comando de paso de bucle para obtener una versión más específica.

Esto puede considerarse un caso especial de un principio más general: i se busca cuando se llama a la función, no cuando se crea; no importa que esto suceda debido a un for círculo. Consulte ¿Qué capturan los cierres de función lambda? para más detalles técnicos.

  • como un recordatorio para mí mismo: docs.python-guide.org/en/latest/writing/gotchas/…

    – Skiptomylu

    10/09/2014 a las 17:20

  • Tenga en cuenta que es posible que el problema no parezca ocurrir usando un generador, si luego itera sobre el generador y llama a cada función. Esto se debe a que todo se evalúa con pereza y, por lo tanto, sucede igual de “tarde” que el enlace. La variable de iteración para los incrementos de bucle, la siguiente función o lambda se crea inmediatamente, y luego se llama inmediatamente a dicha función o lambda, con el valor de iteración actual. Lo mismo se aplica a las expresiones generadoras. Consulte stackoverflow.com/questions/49633868 para ver un ejemplo.

    – Karl Knechtel

    18 ago a las 5:55


avatar de usuario
alex martelli

Estás teniendo un problema con encuadernación tardía — cada función busca i tan tarde como sea posible (por lo tanto, cuando se llama después del final del bucle, i se establecerá en 2).

Se soluciona fácilmente al forzar el enlace anticipado: cambiar def f(): a def f(i=i): como esto:

def f(i=i):
    return i

Los valores predeterminados (la mano derecha i en i=i es un valor predeterminado para el nombre del argumento ique es la mano izquierda i en i=i) son mirados def tiempo, no en call tiempo, por lo que esencialmente son una forma de buscar específicamente el enlace anticipado.

Si estás preocupado por f obtener 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 tu uso de bucle f = make_f(i) en vez de def declaración.

  • ¿Cómo sabes cómo arreglar estas cosas?

    – alwbtc

    18 de agosto de 2018 a las 15:49

  • @alwbtc es principalmente solo experiencia, la mayoría de las personas se han enfrentado a estas cosas por su cuenta en algún momento.

    – ruohola

    5 de marzo de 2019 a las 20:22

  • ¿Puede explicar por qué está funcionando, por favor? (Me salvaste en la devolución de llamada generada en el bucle, los argumentos siempre fueron los últimos del bucle, ¡así que gracias!)

    – Vincent Benet

    29 de julio de 2020 a las 7:32

avatar de usuario
Aran Fey

La explicación

El problema aquí es que el valor de i no se guarda cuando la función f es creado. Bastante, f busca el valor de i cuando es llamó.

Si lo piensas bien, 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á que imprima “bar”, no “foo”, porque el valor de global_var ha cambiado después de que se declaró la función. Lo mismo está sucediendo en su propio código: en el momento en que llama fEl valor de i ha cambiado y se ha configurado para 2.

La solución

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

  • Forzar enlace anticipado de i utilizá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 un poco de información sobre 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, la Actual valor de i se toma una instantánea y se guarda.

    >>> 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,)
    
  • Use una fábrica de funciones para capturar el valor actual de i en un cierre

    La raíz de tu problema es que i es una variable que puede cambiar. Podemos solucionar este problema creando otro variable que está garantizado que nunca cambiará – y la forma más fácil de hacerlo es una 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)
    
  • Usar functools.partial para vincular el valor actual de i a f

    functools.partial le 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 asignar un nuevo valor a la variable. Si usted modificar el objeto almacenado en la variable, volverá a experimentar el mismo 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]

Date cuenta cómo i ¡todavía cambió a pesar de que lo convertimos en un argumento predeterminado! Si tu código muta ientonces debes enlazar un Copiar de i a su función, así:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())

  • el codigo original ya usa cierres, y Python solo crea los cierres entre bastidores. Es solo que cada función creada en el ciclo en el OP, genera su cierre a partir de lo mismo espacio de nombres local; mientras que cada llamada a f_factory crea un nuevo marco de pila con nuevas variables locales, que cada cierre usará por separado. Todavía podemos modificar i *dentro de f_factory después de crear (pero antes de regresar) f.

    – Karl Knechtel

    19 ago a las 9:30

Para agregar a la excelente respuesta de @ Aran-Fey, en la segunda solución, es posible que también desee modificar la variable dentro de su función, lo que se puede lograr con la palabra clave nonlocal:

def f_factory(i):
    def f(offset):
      nonlocal i
      i += offset
      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)
    print(f(10))

Puedes probar así:

l=[]
for t in range(10):
    def up(y):
        print(y)
    l.append(up)
l[5]('printing in 5th function')

¿Ha sido útil esta solución?