Variables locales en funciones anidadas
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.
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_function
por 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 cage
en la get_petters
función.
Cuando realmente llamas a la función, ese cierre se usa para observar el valor de cage
en 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_petters
función ya ha terminado de calcular sus resultados. A la cage
variable 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, cage
contiene 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_cage
función en el bucle, la compilación sólo se realiza una vez, no en cada iteración del bucle.
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.
Esto surge de lo siguiente
for i in range(2):
pass
print(i) # prints 1
después de iterar, el valor de i
se 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.
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 cell
mensajes 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 cell
objeto sigue siendo el mismo durante todas las iteraciones. Sin embargo, como era de esperar, lo específico str
a lo que hace referencia varía en el segundo ciclo. El cell
objeto hace referencia a animal
, que se crea cuando get_petters()
se llama. Sin embargo, animal
cambia a qué str
objeto se refiere cuando se ejecuta la función del generador .
En el primer ciclo, durante cada iteración, creamos todos los f
s, pero solo los llamamos después de que el generador get_petters()
está completamente agotado y una list
de funciones ya está creada.
En el segundo ciclo, durante cada iteración, pausamos el get_petters()
generador y llamamos f
después de cada pausa. Por lo tanto, terminamos recuperando el valor de animal
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,
animal
está mutado y todos los cierres se refieren a lo mismoanimal
.[Nota del editor:
i
se ha cambiado aanimal
.]