Вопрос-Ответ

UnboundLocalError trying to use a variable (supposed to be global) that is (re)assigned (even after first use)

UnboundLocalError пытается использовать переменную (предположительно глобальную), которая (повторно) назначается (даже после первого использования)

Когда я пробую этот код:

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

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

Я получаю сообщение об ошибке из print(c) строки, в которой говорится:

UnboundLocalError: local variable 'c' referenced before assignment

в более новых версиях Python или

UnboundLocalError: 'c' not assigned

в некоторых старых версиях.

Если я закомментирую c += 1, обе print операции будут успешными.

Я не понимаю: почему печать a и b работает, если c нет? Как это c += 1 привело print(c) к сбою, даже когда это происходит позже в коде?

Похоже, что присваивание c += 1 создает локальную переменную c, которая имеет приоритет над глобальной c. Но как переменная может "украсть" область видимости до того, как она существует? Почему здесь c очевидно локально?


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

Смотрите Почему ключевое слово 'global' не требуется для доступа к глобальной переменной? для случаев, когда OP ожидал ошибку, но не получил ее, просто обращаясь к global без global ключевого слова.

Смотрите Как имя может быть "несвязанным" в Python? Какой код может вызвать "UnboundLocalError"? для случаев, когда OP ожидал, что переменная будет локальной, но имеет логическую ошибку, которая предотвращает назначение в каждом конкретном случае.

Переведено автоматически
Ответ 1

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

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

global c

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

Что касается python 3, то теперь

nonlocal c

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

Ответ 2

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 в локальном словаре, не видит его и вставляет в словарь, помечая как неинициализированное. На полностью скомпилированном языке она просто вошла бы в таблицу символов и дождалась синтаксического анализа, но поскольку у нее НЕ будет такой роскоши, как второй проход, лексер выполняет небольшую дополнительную работу, чтобы упростить жизнь позже. , затем он видит ОПЕРАТОР, видит, что правила говорят "если у вас есть operator + = левая сторона должна быть инициализирована" и говорит "упс!"

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

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

Ответ 3

Просмотр дизассемблирования может прояснить, что происходит:

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

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

Ответ 4

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

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

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

2024-01-30 16:08 python