Variables locales en funciones anidadas

Resuelto noio asked hace 12 años • 4 respuestas

Bien, tengan paciencia con esto, sé que va a parecer terriblemente complicado, pero por favor ayúdenme a entender lo que está sucediendo.

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

Da:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Básicamente, ¿por qué no obtengo tres animales diferentes? ¿ No está cage"empaquetado" en el ámbito local de la función anidada? Si no, ¿cómo busca las variables locales una llamada a la función anidada?

Sé que encontrarse con este tipo de problemas generalmente significa que uno "lo está haciendo mal", pero me gustaría entender qué sucede.

noio avatar Sep 14 '12 18:09 noio
Aceptado

La función anidada busca variables del ámbito principal cuando se ejecuta, no cuando se define.

El cuerpo de la función se compila y las variables 'libres' (no definidas en la función misma mediante asignación) se verifican y luego se vinculan como celdas de cierre a la función, y el código usa un índice para hacer referencia a cada celda. pet_functionpor lo tanto, tiene una variable libre ( cage) a la que luego se hace referencia a través de una celda de cierre, índice 0. El cierre en sí apunta a la variable local cageen la get_pettersfunción.

Cuando realmente llamas a la función, ese cierre se usa para observar el valor de cageen el alcance circundante en el momento en que llamas a la función . Aquí radica el problema. En el momento en que llama a sus funciones, la get_pettersfunción ya ha terminado de calcular sus resultados. A la cagevariable local en algún momento durante esa ejecución se le asignó cada una de las cadenas 'cow', 'dog'y 'cat', pero al final de la función, cagecontiene ese último valor 'cat'. Por lo tanto, cuando llama a cada una de las funciones devueltas dinámicamente, obtiene el valor 'cat'impreso.

La solución es no depender de los cierres. En su lugar, puede utilizar una función parcial , crear un nuevo alcance de función o vincular la variable como valor predeterminado para un parámetro de palabra clave .

  • Ejemplo de función parcial, usando functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
    
  • Ejemplo de creación de un nuevo alcance:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
    
  • Vincular la variable como valor predeterminado para un parámetro de palabra clave:

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))
    

No es necesario definir la scoped_cagefunción en el bucle, la compilación sólo se realiza una vez, no en cada iteración del bucle.

Martijn Pieters avatar Sep 14 '2012 11:09 Martijn Pieters

Tengo entendido que se busca jaula en el espacio de nombres de la función principal cuando realmente se llama a la función pet_function obtenida, no antes.

Entonces cuando lo hagas

funs = list(get_petters())

Generas 3 funciones que encontrarán la última jaula creada.

Si reemplaza su último bucle con:

for name, f in get_petters():
    print name + ":", 
    f()

En realidad obtendrás:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey avatar Sep 14 '2012 11:09 Nicolas Barbey

Esto surge de lo siguiente

for i in range(2): 
    pass

print(i)  # prints 1

después de iterar, el valor de ise almacena de forma perezosa como su valor final.

Como generador, la función funcionaría (es decir, imprimir cada valor por turno), pero cuando se transforma en una lista, se ejecuta sobre el generador , por lo tanto, todas las llamadas a cage( cage.animal) devuelven gatos.

Andy Hayden avatar Sep 14 '2012 11:09 Andy Hayden

Simplifiquemos la pregunta. Definir:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

Luego, al igual que en la pregunta, obtenemos:

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Pero si evitamos crear una list()primera:

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

¿Qué está sucediendo? ¿Por qué esta sutil diferencia cambia completamente nuestros resultados?


Si nos fijamos en list(get_petters()), queda claro por el cambio de direcciones de memoria que efectivamente obtenemos tres funciones diferentes:

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

Sin embargo, eche un vistazo a los cellmensajes a los que están vinculadas estas funciones:

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

Para ambos bucles, el cellobjeto sigue siendo el mismo durante todas las iteraciones. Sin embargo, como era de esperar, lo específico stra lo que hace referencia varía en el segundo ciclo. El cellobjeto hace referencia a animal, que se crea cuando get_petters()se llama. Sin embargo, animalcambia a qué strobjeto se refiere cuando se ejecuta la función del generador .

En el primer ciclo, durante cada iteración, creamos todos los fs, pero solo los llamamos después de que el generador get_petters()está completamente agotado y una listde funciones ya está creada.

En el segundo ciclo, durante cada iteración, pausamos el get_petters()generador y llamamos fdespués de cada pausa. Por lo tanto, terminamos recuperando el valor de animalen ese momento en el que la función del generador está en pausa.

Como @Claudiu responde a una pregunta similar :

Se crean tres funciones separadas, pero cada una tiene el cierre del entorno en el que están definidas; en este caso, el entorno global (o el entorno de la función externa si el bucle se coloca dentro de otra función). Sin embargo, este es exactamente el problema: en este entorno, animalestá mutado y todos los cierres se refieren a lo mismo animal.

[Nota del editor: ise ha cambiado a animal.]

Mateen Ulhaq avatar Jan 20 '2020 16:01 Mateen Ulhaq