Почему 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.