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

105

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

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
Попробуй for animal in ['cat', 'dog', 'cow']... Я уверен, что кто-нибудь придет и объяснит это - это одна из тех проблем с Python :)
Джон Клементс

Ответы:

114

Вложенная функция ищет переменные из родительской области при выполнении, а не при определении.

Тело функции компилируется, и «свободные» переменные (не определенные в самой функции путем присваивания) проверяются, затем привязываются к функции в качестве закрывающих ячеек, причем код использует индекс для ссылки на каждую ячейку. 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функцию в цикле, компиляция выполняется только один раз, а не на каждой итерации цикла.

Мартейн Питерс
источник
1
Я сегодня 3 часа бился головой об эту стену над сценарием для работы. Ваше последнее замечание очень важно, и это основная причина, по которой я столкнулся с этой проблемой. У меня в изобилии есть обратные вызовы с замыканиями по всему моему коду, но я попробовал ту же технику в цикле.
DrEsperanto
12

Я понимаю, что клетка ищется в пространстве имен родительской функции, когда фактически вызывается полученная функция 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.
Николас Барби
источник
6

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

for i in range(2): 
    pass

print(i)  # prints 1

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

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

Энди Хайден
источник
0

Упростим вопрос. Определите:

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

        yield (animal, pet_function)

Тогда, как и в вопросе, получаем:

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

Но если мы избегаем создания list()первого:

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

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

В чем дело? Почему эта тонкая разница полностью меняет наши результаты?


Если мы посмотрим list(get_petters()), то по изменению адресов памяти станет ясно, что мы действительно передаем три разные функции:

>>> 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>)]

Однако взгляните на cells, с которыми связаны эти функции:

>>> 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>,)

Для обоих циклов cellобъект остается неизменным на всех итерациях. Однако, как и ожидалось, конкретное, на что strон ссылается, меняется во втором цикле. Ссылается на cellобъект animal, который создается при get_petters()вызове. Однако при запуске функции генератораanimal изменяется, к какому strобъекту он относится .

В первом цикле во время каждой итерации мы создаем все fs, но вызываем их только после того, как генератор get_petters()полностью исчерпан и listуже создан a of functions.

Во втором цикле во время каждой итерации мы приостанавливаем get_petters()генератор и вызываем его fпосле каждой паузы. Таким образом, мы получаем значение animalв тот момент времени, когда функция генератора приостановлена.

Как @Claudiu задает ответ на аналогичный вопрос :

Создаются три отдельные функции, но каждая из них закрывает среду, в которой они определены, - в данном случае глобальную среду (или среду внешней функции, если цикл помещен внутри другой функции). Но проблема именно в этом - в этой среде animalпроисходит мутация, и все замыкания относятся к одному и тому же animal.

[Примечание редактора: iбыло изменено на animal.]

Матин Улхак
источник