Почему Python не может увеличивать переменную в закрытии?
В этом фрагменте кода я могу напечатать значение счетчика из функции бара
def foo():
counter = 1
def bar():
print("bar", counter)
return bar
bar = foo()
bar()
Но когда я пытаюсь увеличить счетчик внутри функции бара, я получаю ошибку UnboundLocalError.
UnboundLocalError: local variable 'counter' referenced before assignment
Фрагмент кода с оператором приращения в.
def foo():
counter = 1
def bar():
counter += 1
print("bar", counter)
return bar
bar = foo()
bar()
У вас есть доступ только для чтения к переменным во внешней функции в закрытии Python?
Ответы
Ответ 1
Вы не можете изменить переменные замыкания в Python 2. В Python 3, который вы, похоже, используете из-за своего print()
, вы можете объявить их nonlocal
:
def foo():
counter = 1
def bar():
nonlocal counter
counter += 1
print("bar", counter)
return bar
bar = foo()
bar()
В противном случае, присваивание в bar()
делает переменную локальной, и, поскольку вы не присвоили значение переменной в локальной области видимости, попытка получить доступ к ней является ошибкой.
В Python 2 мой любимый обходной путь выглядит так:
def foo():
class nonlocal:
counter = 1
def bar():
nonlocal.counter += 1
print("bar", nonlocal.counter)
return bar
bar = foo()
bar()
Это работает, потому что изменение мутабельного объекта не требует изменения того, на что указывает имя переменной. В этом случае nonlocal
является переменная замыкания, и она никогда не переназначается; изменено только его содержимое. Другие обходные пути используют списки или словари.
Или вы можете использовать класс для всего этого, как предлагает @naomik в комментарии. Определите __call__()
чтобы сделать экземпляр вызываемым.
class Foo(object):
def __init__(self, counter=1):
self.counter = counter
def __call__(self):
self.counter += 1
print("bar", self.counter)
bar = Foo()
bar()
Ответ 2
Почему Python не может увеличивать переменную в закрытии?
Сообщение об ошибке самоочевидно. Здесь я предлагаю несколько решений.
- Использование атрибута функции (необычно, но работает очень хорошо)
- Использование замыкания с
nonlocal
(идеальным, но только Python 3) - Использование замыкания над изменяемым объектом (идиоматическим для Python 2)
- Использование метода на пользовательском объекте
- Прямой вызов экземпляра объекта путем реализации
__call__
Используйте атрибут функции.
Установите атрибут счетчика на свою функцию вручную после его создания:
def foo():
foo.counter += 1
return foo.counter
foo.counter = 0
И сейчас:
>>> foo()
1
>>> foo()
2
>>> foo()
3
Или вы можете автоматически установить функцию:
def foo():
if not hasattr(foo, 'counter'):
foo.counter = 0
foo.counter += 1
return foo.counter
По аналогии:
>>> foo()
1
>>> foo()
2
>>> foo()
3
Эти подходы просты, но необычны и вряд ли будут быстро подвергнуты риску, если кто-то просмотрит ваш код без вашего присутствия.
Более распространенные способы, которые вы хотите выполнить, варьируются в зависимости от вашей версии Python.
Python 3, используя закрытие с nonlocal
В Python 3 вы можете объявить нелокальным:
def foo():
counter = 0
def bar():
nonlocal counter
counter += 1
print("bar", counter)
return bar
bar = foo()
И он будет увеличиваться
>>> bar()
bar 1
>>> bar()
bar 2
>>> bar()
bar 3
Это, вероятно, самое идиоматическое решение этой проблемы. Жаль, что он ограничен Python 3.
Обходной путь Python 2 для нелокальных:
Вы можете объявить глобальную переменную, а затем увеличивать ее, но это загромождает пространство имен модулей. Таким образом, идиоматическое обходное решение, чтобы избежать объявления глобальной переменной, - это указать на изменяемый объект, который содержит целое число, на которое вы хотите увеличить, чтобы вы не пытались переназначить имя переменной:
def foo():
counter = [0]
def bar():
counter[0] += 1
print("bar", counter)
return bar
bar = foo()
и сейчас:
>>> bar()
('bar', [1])
>>> bar()
('bar', [2])
>>> bar()
('bar', [3])
Я думаю, что это лучше, чем предложения, связанные с созданием классов только для того, чтобы удерживать вашу добавочную переменную. Но чтобы быть полным, посмотрим.
Использование пользовательского объекта
class Foo(object):
def __init__(self):
self._foo_call_count = 0
def foo(self):
self._foo_call_count += 1
print('Foo.foo', self._foo_call_count)
foo = Foo()
и сейчас:
>>> foo.foo()
Foo.foo 1
>>> foo.foo()
Foo.foo 2
>>> foo.foo()
Foo.foo 3
или даже реализовать __call__
:
class Foo2(object):
def __init__(self):
self._foo_call_count = 0
def __call__(self):
self._foo_call_count += 1
print('Foo', self._foo_call_count)
foo = Foo2()
и сейчас:
>>> foo()
Foo 1
>>> foo()
Foo 2
>>> foo()
Foo 3
Ответ 3
Поскольку вы не можете изменять неизменяемые объекты в Python, в Python 2.X используется обходной путь:
Использовать список
def counter(start):
count = [start]
def incr():
count[0] += 1
return count[0]
return incr
a = counter(100)
print a()
b = counter(200)
print b()
Ответ 4
потому что в
def bar():
print("bar", counter)
Счетчик рассматривается в качестве переменной, которая является локальной для bar
не один из foo
.
Ответ 5
nonlocal
- это новый оператор в Python 3, и в Python 2 нет эквивалента.
Полезная ссылка Общие сведения о UnboundLocalError в Python