Локальные переменные во вложенных функциях
Хорошо, потерпите меня, я знаю, что это будет выглядеть ужасно запутанно, но, пожалуйста, помогите мне понять, что происходит.
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()
Дает:
cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.
Итак, в принципе, почему я не получаю трех разных животных? Разве cage
не "упакован" в локальную область вложенной функции? Если нет, то как вызов вложенной функции выполняет поиск локальных переменных?
Я знаю, что столкновение с такого рода проблемами обычно означает, что кто-то "делает это неправильно", но я хотел бы понять, что происходит.
Переведено автоматически
Ответ 1
Вложенная функция ищет переменные из родительской области при выполнении, а не при определении.
Тело функции компилируется, и "свободные" переменные (не определенные в самой функции путем присваивания) проверяются, затем привязываются как ячейки закрытия к функции, при этом код использует индекс для ссылки на каждую ячейку. pet_function
таким образом, есть одна свободная переменная (cage
), на которую затем ссылаются через закрывающую ячейку с индексом 0. Само замыкание указывает на локальную переменную cage
в get_petters
функции.
Когда вы на самом деле вызываете функцию, это замыкание затем используется для просмотра значения cage
в окружающей области во время вызова функции. Вот в чем проблема. К тому времени, когда вы вызываете свои функции, get_petters
функция уже закончила вычислять свои результаты. cage
Локальной переменной в определенный момент выполнения была присвоена каждая из 'cow'
, 'dog'
и 'cat'
строк, но в конце функции cage
содержит это последнее значение 'cat'
. Таким образом, при вызове каждой из динамически возвращаемых функций вы получаете выведенное значение 'cat'
.
Обходной путь заключается в том, чтобы не полагаться на замыкания. Вместо этого вы можете использовать частичную функцию, создать новую область действия функции или привязать переменную как значение по умолчанию для параметра ключевого слова.
Пример частичной функции с использованием
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)))Пример создания новой области видимости:
def scoped_cage(cage=None):
def pet_function():
print "Mary pets the " + cage.animal + "."
return pet_function
yield (animal, partial(gotimes, scoped_cage(cage)))Привязка переменной в качестве значения по умолчанию для параметра ключевого слова:
def pet_function(cage=cage):
print "Mary pets the " + cage.animal + "."
yield (animal, partial(gotimes, pet_function))
Нет необходимости определять scoped_cage
функцию в цикле, компиляция выполняется только один раз, а не на каждой итерации цикла.
Ответ 2
My understanding is that cage is looked for in the parent function namespace when the yielded pet_function is actually called, not before.
So when you do
funs = list(get_petters())
You generate 3 functions which will find the lastly created cage.
If you replace your last loop with :
for name, f in get_petters():
print name + ":",
f()
You will actually get :
cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
Ответ 3
This stems from the following
for i in range(2):
pass
print(i) # prints 1
after iterating the value of i
is lazily stored as its final value.
As a generator the function would work (i.e. printing each value in turn), but when transforming to a list it runs over the generator, hence all calls to cage
(cage.animal
) return cats.
Ответ 4
Let's simplify the question. Define:
def get_petters():
for animal in ['cow', 'dog', 'cat']:
def pet_function():
return "Mary pets the " + animal + "."
yield (animal, pet_function)
Then, just like in the question, we get:
>>> 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.
But if we avoid creating a list()
first:
>>> for name, f in get_petters():
... print(name + ":", f())
cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.
What's going on? Why does this subtle difference completely change our results?
If we look at list(get_petters())
, it's clear from the changing memory addresses that we do indeed yield three different functions:
>>> 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>)]
However, take a look at the cell
s that these functions are bound to:
>>> 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>,)
For both loops, the cell
object remains the same throughout the iterations. However, as expected, the specific str
it references varies in the second loop. The cell
object refers to animal
, which is created when get_petters()
is called. However, animal
changes what str
object it refers to as the generator function runs.
In the first loop, during each iteration, we create all the f
s, but we only call them after the generator get_petters()
is completely exhausted and a list
of functions is already created.
In the second loop, during each iteration, we are pausing the get_petters()
generator and calling f
after each pause. Thus, we end up retrieving the value of animal
at that moment in time that the generator function is paused.
As @Claudiu puts in an answer to a similar question:
Three separate functions are created, but they each have the closure of the environment they're defined in - in this case, the global environment (or the outer function's environment if the loop is placed inside another function). This is exactly the problem, though -- in this environment,
animal
is mutated, and the closures all refer to the sameanimal
.[Editor note:
i
has been changed toanimal
.]