Variables locales en funciones anidadas

105

De acuerdo, tengan paciencia conmigo en esto, sé que se verá horriblemente 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.

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

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

noio
fuente
1
Inténtalo for animal in ['cat', 'dog', 'cow']... Estoy seguro de que alguien vendrá y explicará esto, es uno de esos problemas de Python :)
Jon Clements

Respuestas:

114

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 en sí por asignación), se verifican, luego se unen como celdas de cierre a la función, con el código usando un índice para hacer referencia a cada celda. pet_functionpor lo tanto, tiene una variable libre ( cage) que luego se 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 llama a la función, ese cierre se usa para ver el valor de cageen el ámbito circundante en el momento en que llama a la función . Aquí radica el problema. Cuando llame a sus funciones, la get_pettersfunción ya habrá terminado de calcular sus resultados. La cagevariable local en algún momento de que la ejecución se asigna a cada uno de los 'cow', 'dog'y 'cat'cadenas, 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 ámbito 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)))
  • Creación de un nuevo ejemplo de 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 solo se realiza una vez, no en cada iteración del bucle.

Martijn Pieters
fuente
1
Hoy me golpeé la cabeza contra esta pared durante 3 horas en un guión para el trabajo. Su último punto es muy importante y es la razón principal por la que encontré este problema. Tengo devoluciones de llamada con cierres en abundancia en todo mi código, pero probar la misma técnica en un bucle es lo que me atrapó.
DrEsperanto
12

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

Así que 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()

Realmente obtendrás:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Nicolas Barbey
fuente
6

Esto se deriva de lo siguiente

for i in range(2): 
    pass

print(i)  # prints 1

después de iterar el valor de i se almacena perezosamente como su valor final.

Como generador, la función funcionaría (es decir, imprimir cada valor a su vez), 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
fuente
0

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)

Entonces, 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.

¿Que esta pasando? ¿Por qué esta sutil diferencia cambia completamente nuestros resultados?


Si miramos list(get_petters()), está claro por las direcciones de memoria cambiantes que de hecho producimos 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 cells 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 permanece igual a lo largo de las iteraciones. Sin embargo, como era de esperar, el específico al strque hace referencia varía en el segundo ciclo. El cellobjeto al animalque se refiere , que se crea cuando get_petters()se llama. Sin embargo, animalcambia el strobjeto al que se refiere mientras 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 unlist ya se ha creado función.

En el segundo ciclo, durante cada iteración, estamos pausando el get_petters()generador y llamando fdespués de cada pausa. Así, terminamos recuperando el valor deanimal en 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 al mismoanimal .

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

Mateen Ulhaq
fuente