Подтвердить что ты не робот

Почему это закрытие не изменяет переменную в охватывающей области?

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

def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that 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?

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

4b9b3361

Ответ 1

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

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

Ответ 2

Есть два "лучших" /более 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 и использует тот факт, что в этой ситуации у вас уже есть объект, который вы знаете, имя которого вы можете ссылаться на атрибуты: нет необходимости создавать новый контейнер только для этой цели.

Ответ 3

Пример

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

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

Ответ 4

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