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

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

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 не «упаковано» в локальную область вложенной функции? Если нет, то как вызов вложенной функции ищет локальные переменные?

Я знаю, что столкновение с такого рода проблемами обычно означает, что кто-то «делает это неправильно», но я хотел бы понять, что происходит.


person noio    schedule 14.09.2012    source источник
comment
Попробуйте for animal in ['cat', 'dog', 'cow']... Я уверен, что кто-нибудь придет и объяснит это - это одна из тех ошибок Python :)   -  person Jon Clements♦    schedule 14.09.2012


Ответы (4)


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

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

person Martijn Pieters    schedule 14.09.2012
comment
Я сегодня часа 3 билась головой об эту стену над скриптом для работы. Ваш последний пункт очень важен и является основной причиной, по которой я столкнулся с этой проблемой. У меня множество обратных вызовов с замыканиями по всему коду, но попытка использовать ту же технику в цикле — вот что меня зацепило. - person DrEsperanto; 07.03.2018

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

Это вытекает из следующего

for i in range(2): 
    pass

print(i)  # prints 1

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

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

person Andy Hayden    schedule 14.09.2012

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

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

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

>>> 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, на который он ссылается, по мере выполнения функции-генератора.

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

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

Как @Claudiu отвечает на похожий вопрос:

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

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

person Mateen Ulhaq    schedule 20.01.2020