Локальные переменные в вложенных функциях Python

Хорошо, медведь со мной на этом, я знаю, что это будет выглядеть ужасно запутанным, но, пожалуйста, помогите мне понять, что происходит.

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 образом, 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

Мое понимание заключается в том, что клетка просматривается в пространстве имен родительских функций, когда вызываемая функция pet_function фактически вызывается, а не раньше.

Итак, когда вы делаете

funs = list(get_petters())

Вы создаете 3 функции, которые найдут последнюю созданную клетку.

Если вы замените свой последний цикл следующим образом:

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

Фактически вы получите:

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

Ответ 3

Это происходит из следующего

for i in range(2): 
    pass

print i is 1

после итерации значение i лениво сохраняется как его окончательное значение.

В качестве генератора функция будет работать (т.е. печатать каждое значение по очереди), но при преобразовании в список, который он запускает через генератор, поэтому все вызовы cage (cage.animal) вернуть кошек.