UnboundLocalError для локальной переменной при переназначении после первого использования

210

Следующий код работает как положено в Python 2.5 и 3.0:

a, b, c = (1, 2, 3)

print(a, b, c)

def test():
    print(a)
    print(b)
    print(c)    # (A)
    #c+=1       # (B)
test()

Однако, когда я раскомментирую строку (B) , я получаю UnboundLocalError: 'c' not assignedстроку (A) . Значения aи bнапечатаны правильно. Это полностью сбило меня с толку по двум причинам:

  1. Почему в строке (A) возникает ошибка времени выполнения из-за более позднего оператора в строке (B) ?

  2. Почему переменные aи bпечатаются как положено, а cвыдает ошибку?

Единственное объяснение, которое я могу придумать, заключается в том , что присваивание создает локальную переменную , которая имеет прецедент над «глобальной» переменной еще до создания локальной переменной. Конечно, для переменной не имеет смысла «красть» область видимости до того, как она существует.cc+=1c

Может ли кто-нибудь объяснить это поведение?

TBA
источник
Отвечает ли это на ваш вопрос? Не понимаю, почему происходит UnboundLocalError (закрытие)
norok2

Ответы:

217

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

Если вы хотите, чтобы переменная cссылалась на глобальное значение, c = 3назначенное перед функцией, поместите

global c

в первой строке функции.

Что касается питона 3, то сейчас

nonlocal c

что вы можете использовать для ссылки на ближайшую область действия функции, которая имеет cпеременную.

рекурсивный
источник
3
Спасибо. Быстрый вопрос. Означает ли это, что Python решает область действия каждой переменной перед запуском программы? Перед запуском функции?
TBA
7
Решение о переменной области действия принимается компилятором, который обычно запускается один раз при первом запуске программы. Однако стоит помнить, что компилятор может также запуститься позже, если в вашей программе есть операторы «eval» или «exec».
Грег Хьюгилл
2
Хорошо, спасибо. Я предполагаю, что «интерпретируемый язык» не подразумевает так много, как я думал.
TBA
1
Ах, это «нелокальное» ключевое слово было именно то, что я искал, казалось, Python пропустил это. Предположительно, это 'каскады' через каждую вмещающую область, которая импортирует переменную, используя это ключевое слово?
Брендан
6
@brainfsck: это легче понять, если провести различие между «поиском» и «назначением» переменной. Поиск возвращается к более высокой области, если имя не найдено в текущей области. Назначение всегда выполняется в локальной области (если вы не используете globalили nonlocalдля принудительного назначения глобального или нелокального назначения)
Стивен
71

Python немного странный в том, что он хранит все в словаре для различных областей. Оригинал a, b, c находится в самой верхней области видимости и, следовательно, в этом самом верхнем словаре. Функция имеет свой словарь. Когда вы достигнете print(a)и print(b)заявления, нет ничего под этим именем в словаре, поэтому Python просматривает список и находит их в глобальном словаре.

Теперь мы получаем c+=1, что, конечно, эквивалентно c=c+1. Когда Python просматривает эту строку, он говорит: «Ага, есть переменная с именем c, я помещу ее в свой локальный словарь области видимости». Затем, когда он ищет значение c для c в правой части присваивания, он находит свою локальную переменную с именем c , которая еще не имеет значения, и поэтому выдает ошибку.

global cУпомянутое выше утверждение просто говорит парсеру, что он использует cглобальную область видимости и поэтому не нуждается в новой.

Причина, по которой он говорит, что есть проблема в строке, которую он делает, заключается в том, что он эффективно ищет имена, прежде чем попытается сгенерировать код, и поэтому в некотором смысле не думает, что он действительно делает эту строку еще. Я бы сказал, что это ошибка юзабилити, но, как правило, полезно просто учиться не воспринимать сообщения компилятора слишком серьезно.

Если это утешит, я провел, вероятно, целый день, копая и экспериментируя с этой же проблемой, прежде чем нашел что-то, что Гвидо написал о словарях, которые объяснили все.

Обновление, смотрите комментарии:

Он не сканирует код дважды, но он сканирует код в два этапа: лексирование и анализ.

Рассмотрим, как работает синтаксический анализ этой строки кода. Лексер читает исходный текст и разбивает его на лексемы, «самые маленькие компоненты» грамматики. Поэтому, когда он попадает в линию

c+=1

это разбивает его на что-то вроде

SYMBOL(c) OPERATOR(+=) DIGIT(1)

Парсер в конечном итоге хочет превратить это в дерево разбора и выполнить его, но, поскольку это присваивание, до этого он ищет имя c в локальном словаре, не видит его и вставляет его в словарь, отмечая это как неинициализированный. На полностью скомпилированном языке он просто заходил бы в таблицу символов и ждал разбора, но, поскольку у него не было бы роскоши второго прохода, лексер проделал небольшую дополнительную работу, чтобы облегчить жизнь в дальнейшем. Только тогда он видит ОПЕРАТОРА, видит, что в правилах написано «если у вас есть оператор + = левая сторона должна быть инициализирована», и говорит «упс!»

Дело в том, что он еще не начал анализ строки . Все это происходит как бы перед подготовкой к фактическому анализу, поэтому счетчик строк не перешел на следующую строку. Таким образом, когда он сигнализирует об ошибке, он все еще думает, что на предыдущей строке.

Как я уже сказал, вы можете утверждать, что это ошибка юзабилити, но на самом деле это довольно распространенная вещь. Некоторые компиляторы более честны по этому поводу и говорят «ошибка в строке XXX или около нее», но это не так.

Чарли Мартин
источник
1
Хорошо, спасибо за ваш ответ; это прояснило мне кое-что о возможностях в python. Тем не менее, я до сих пор не понимаю, почему ошибка возникает в строке (A), а не в строке (B). Создает ли Python свой словарь переменной области ДО запуска программы?
TBA
1
Нет, это на уровне выражения. Я добавлю к ответу, я не думаю, что смогу уместить это в комментарии.
Чарли Мартин
2
Примечание к деталям реализации: В CPython локальная область обычно не обрабатывается как a dict, а внутренне является просто массивом ( locals()заполняет a dictдля возврата, но изменения в нем не создают нового locals). Фаза синтаксического анализа заключается в нахождении каждого назначения для локального и преобразовании из имени в позицию в этом массиве, и использовании этой позиции всякий раз, когда ссылается на имя. При входе в функцию локальные UnboundLocalErrorпеременные без аргументов инициализируются заполнителем, и s происходят, когда переменная читается, а связанный с ней индекс по-прежнему имеет значение заполнителя.
ShadowRanger
44

Взглянув на разборку, можно уточнить, что происходит:

>>> def f():
...    print a
...    print b
...    a = 1

>>> import dis
>>> dis.dis(f)

  2           0 LOAD_FAST                0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE

  3           5 LOAD_GLOBAL              0 (b)
              8 PRINT_ITEM
              9 PRINT_NEWLINE

  4          10 LOAD_CONST               1 (1)
             13 STORE_FAST               0 (a)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE

Как вы можете видеть, байткод для доступа к это LOAD_FAST, и для Ь LOAD_GLOBAL. Это связано с тем, что компилятор определил, что a назначен внутри функции, и классифицировал его как локальную переменную. Механизм доступа для локальных пользователей принципиально отличается для глобальных переменных - им статически назначается смещение в таблице переменных фрейма, что означает, что поиск является быстрым индексом, а не более дорогим поиском разборчивости, как для глобальных. Из-за этого Python читает print aстроку как «получить значение локальной переменной« a », хранящейся в слоте 0, и распечатать его», и, когда он обнаруживает, что эта переменная все еще не инициализирована, вызывает исключение.

Брайан
источник
10

У Python довольно интересное поведение, когда вы используете традиционную семантику глобальных переменных. Я не помню деталей, но вы можете просто прочитать значение переменной, объявленной в «глобальной» области видимости, но если вы хотите изменить ее, вам нужно использовать globalключевое слово. Попробуйте перейти test()на это:

def test():
    global c
    print(a)
    print(b)
    print(c)    # (A)
    c+=1        # (B)

Кроме того, причина того, что вы получаете эту ошибку, заключается в том, что вы также можете объявить новую переменную внутри этой функции с тем же именем, что и у «глобальной», и она будет полностью отдельной. Интерпретатор считает, что вы пытаетесь создать новую переменную в этой области cи вызвать ее изменение за одну операцию, что не разрешено в Python, потому что эта новая cне была инициализирована.

Мангуста
источник
Спасибо за ваш ответ, но я не думаю, что это объясняет, почему ошибка выдается в строке (A), где я просто пытаюсь напечатать переменную. Программа никогда не попадает в строку (B), где она пытается изменить неинициализированную переменную.
TBA
1
Python прочитает, проанализирует и превратит всю функцию во внутренний байт-код до того, как запустит программу, поэтому тот факт, что «превращение c в локальную переменную» происходит текстуально после печати значения, как бы не имеет значения.
Vatine
6

Лучший пример, который проясняет это:

bar = 42
def foo():
    print bar
    if False:
        bar = 0

при вызове foo()это также повышается, UnboundLocalError хотя мы никогда не достигнем строки bar=0, поэтому логически локальная переменная никогда не должна создаваться.

Тайна кроется в « Python - это интерпретируемый язык », и объявление функции fooинтерпретируется как одно утверждение (т. Е. Составное утверждение), оно просто тупо интерпретирует его и создает локальные и глобальные области видимости. Так barпризнается в локальной области до исполнения.

Для большего количества примеров как это Прочитайте этот пост: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Этот пост содержит полное описание и анализ Python Scoping переменных:

Сахил Калра
источник
5

Вот две ссылки, которые могут помочь

1: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

2: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call-by-reference

Первая ссылка описывает ошибку UnboundLocalError. Вторая ссылка может помочь с переписыванием вашей тестовой функции. Исходя из второй ссылки, исходную проблему можно переписать так:

>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
...     print (a)
...     print (b)
...     print (c)
...     c += 1
...     return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)
mcdon
источник
4

Это не прямой ответ на ваш вопрос, но он тесно связан, так как это еще один недостаток, вызванный взаимосвязью между расширенным назначением и областями функций.

В большинстве случаев вы склонны думать о расширенном назначении ( a += b) как о точном эквиваленте простого назначения ( a = a + b). Впрочем, с этим можно столкнуться и в одном случае. Позволь мне объяснить:

То, как работает простое присваивание Python, означает, что если aон передается в функцию (например func(a), обратите внимание, что Python всегда передается по ссылке), он a = a + bне будет изменять переданный объект a. Вместо этого он просто изменит локальный указатель на a.

Но если вы используете a += b, то это иногда реализуется как:

a = a + b

или иногда (если метод существует) как:

a.__iadd__(b)

В первом случае (до тех пор, пока aон не объявлен глобальным), за пределами локальной области видимости нет никаких побочных эффектов, поскольку присваивание aявляется просто обновлением указателя.

Во втором случае aфактически изменится сам, поэтому все ссылки на aбудут указывать на измененную версию. Это демонстрируется следующим кодом:

def copy_on_write(a):
      a = a + a
def inplace_add(a):
      a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1

Так что хитрость заключается в том, чтобы избежать расширенного присваивания аргументов функции (я стараюсь использовать его только для локальных переменных / переменных цикла). Используйте простое задание, и вы будете защищены от неоднозначного поведения.

alsuren
источник
2

Интерпретатор Python прочитает функцию как единое целое. Я думаю об этом как о чтении его в два прохода, один раз, чтобы собрать его замыкание (локальные переменные), затем снова превратить его в байт-код.

Как я уверен, вы уже знали, что любое имя слева от «=» неявно является локальной переменной. Не раз меня ловили, меняя доступ к переменной на + =, и вдруг это была другая переменная.

Я также хотел отметить, что это не имеет ничего общего с глобальным охватом. Вы получаете то же самое поведение с вложенными функциями.

Джеймс Хопкин
источник
2

c+=1назначает c, python предполагает, что назначенные переменные являются локальными, но в этом случае он не был объявлен локально.

Или используйте ключевые слова globalили nonlocal.

nonlocal работает только в Python 3, поэтому, если вы используете Python 2 и не хотите делать переменную глобальной, вы можете использовать изменяемый объект:

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()
Colegram
источник
1

Лучший способ получить доступ к переменной класса - это прямой доступ по имени класса.

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
Харун ЭРГУЛЬ
источник
0

В Python у нас есть аналогичное объявление для всех типов переменных: локальные, переменные класса и глобальные переменные. когда вы ссылаетесь на глобальную переменную из метода, python думает, что вы на самом деле ссылаетесь на переменную из самого метода, который еще не определен, и, следовательно, выдает ошибку. Чтобы ссылаться на глобальную переменную, мы должны использовать globals () ['variableName'].

в вашем случае используйте globals () ['a], globals () [' b '] и globals () [' c '] вместо a, b и c соответственно.

Сантош Кадам
источник
0

Та же проблема беспокоит меня. Использование nonlocalи globalможет решить проблему.
Тем не менее, внимание, необходимое для использования nonlocal, это работает для вложенных функций. Однако на уровне модуля это не работает. Смотрите примеры здесь.

Циньшэн Чжан
источник