Creación de funciones en un bucle

5 minutos de lectura

Creacion de funciones en un bucle
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?

1646754429 667 Creacion de funciones en un bucle
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(): para def f(i=i): Me gusta 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

Creacion de funciones en un bucle
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 llamado.

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)
    
  • Utilizar functools.partial para vincular el valor actual de i para 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())

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?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad