Ошибка области переменной Python

Следующий код работает как в 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 печатаются правильно. Это меня полностью сбило с толку по двум причинам:

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

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

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

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

Ответ 1

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

Если вы хотите, чтобы переменная c ссылалась на глобальный c put

global c

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

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

nonlocal c

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

Ответ 2

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

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

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

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

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

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

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

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

c+=1

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

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

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

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

Как я уже сказал, вы можете утверждать, что это ошибка юзабилити, но на самом деле это довольно обычная вещь. Некоторые компиляторы более честны в этом вопросе и говорят "ошибка в строке 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 не был инициализирован.

Ответ 5

Лучший пример, который дает понять:

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. Определение переменных:

Ответ 6

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

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. Ссылка 2 может помочь с повторной записью тестовой функции. На основе второй ссылки исходная проблема может быть переписана как:

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

Ответ 7

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

В большинстве случаев вы склонны думать о расширенном назначении (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

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

Лучший способ получить переменную класса напрямую связан с именем класса

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1

Ответ 11

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

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

Ответ 12

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