Почему это замыкание не изменяет переменную во вложенной области видимости? - PullRequest
19 голосов
/ 24 сентября 2011

Этот бит Python не работает:

def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that's not my point though (:
        while True:
            yield start
            start += 1
    return closure

x = make_incrementer(100)
iter = x()
print iter.next()    # Exception: UnboundLocalError: local variable 'start' referenced before assignment

Я знаю, как исправить эту ошибку, но потерпите меня:

Этот код отлично работает:

def test(start):
    def closure():
        return start
    return closure

x = test(999)
print x()    # prints 999

Почему я могу читать переменную start внутри замыкания, но не записывать в нее? Какое правило языка вызывает эту обработку переменной start?

Обновление : я нашел этот пост таким релевантным (ответ больше, чем вопрос): Чтение / запись закрытий Python

Ответы [ 4 ]

30 голосов
/ 24 сентября 2011

Всякий раз, когда вы назначаете переменную внутри функции, она будет локальной переменной для этой функции.Строка start += 1 присваивает новое значение start, поэтому start является локальной переменной.Поскольку существует локальная переменная start, функция не будет пытаться искать в глобальной области действия start при первой попытке доступа к ней, поэтому вы видите ошибку.

В 3.x вашем кодеПример будет работать, если вы используете ключевое слово nonlocal:

def make_incrementer(start):
    def closure():
        nonlocal start
        while True:
            yield start
            start += 1
    return closure

В 2.x вы часто можете обойти подобные проблемы, используя ключевое слово global, но здесь это не работает, потому что startне является глобальной переменной.

В этом сценарии вы можете сделать что-то похожее на то, что вы предложили (x = start), или использовать изменяемую переменную, в которой вы изменяете и выдает внутреннее значение.

9 голосов
/ 24 сентября 2011

Есть два «лучших» / более Pythonic способа сделать это на Python 2.x, чем использовать контейнер просто для того, чтобы обойти отсутствие нелокального ключевого слова.

Тот, который вы упомянули в комментарии в своем коде - привязка к локальной переменной. Есть еще один способ сделать это:

Использование аргумента по умолчанию

def make_incrementer(start):
    def closure(start = start):
        while True:
            yield start
            start += 1
    return closure

x = make_incrementer(100)
iter = x()
print iter.next()

В этом есть все преимущества локальной переменной без дополнительной строки кода. Это также происходит в строке x = make_incrememter(100), а не в строке iter = x(), что может иметь или не иметь значения в зависимости от ситуации.

Вы также можете использовать метод «на самом деле не присваивать ссылочной переменной» более элегантным способом, чем использование контейнера:

Использование атрибута функции

def make_incrementer(start):
    def closure():
        # You can still do x = closure.start if you want to rebind to local scope
        while True:
            yield closure.start
            closure.start += 1
    closure.start = start
    return closure

x = make_incrementer(100)
iter = x()
print iter.next()    

Это работает во всех последних версиях Python и использует тот факт, что в этой ситуации у вас уже есть объект, имя которого вы можете использовать для ссылок на атрибуты - нет необходимости создавать новый контейнер только для этой цели .

4 голосов
/ 24 сентября 2011

Пример

def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that's not my point though (:
        while True:
            yield start[0]
            start[0] += 1
    return closure

x = make_incrementer([100])
iter = x()
print iter.next()
3 голосов
/ 24 сентября 2011

В Python 3.x вы можете использовать ключевое слово nonlocal для повторного связывания имен вне локальной области.В 2.x вашими единственными опциями являются изменение (или мутирование ) переменных закрытия, добавление переменных экземпляра во внутреннюю функцию или (как вы этого не хотите) созданиелокальная переменная ...

# modifying  --> call like x = make_incrementer([100])
def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that's not my point though (:
        while True:
            yield start[0]
            start[0] += 1
    return closure

# adding instance variables  --> call like x = make_incrementer(100)
def make_incrementer(start):
    def closure():
        while True:
            yield closure.start
            closure.start += 1
    closure.start = start
    return closure

# creating local variable  --> call like x = make_incrementer(100)
def make_incrementer(start):
    def closure(start=start):
        while True:
            yield start
            start += 1
    return closure
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...