Локальні змінні в вкладених функціях


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']... Я впевнений, що хтось підійде і пояснить це, хоча - це один із тих пітхонів :)
Джон Клементс

Відповіді:


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функція вже створена.

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

Як @Claudiu ставить відповідь на подібне запитання :

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

[Примітка редактора: iзмінено на animal.]

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.