Почему это закрытие не изменяет переменную в охватывающей области?
Этот бит 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
Ответы
Ответ 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