Ошибка области видимости переменной Python - PullRequest
190 голосов
/ 16 декабря 2008

Следующий код работает как положено в 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 вызывает ошибку?

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

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

Ответы [ 11 ]

197 голосов
/ 16 декабря 2008

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

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

global c

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

Что касается Python 3, то сейчас

nonlocal c

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

67 голосов
/ 16 декабря 2008

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 или около нее», но этого нет.

43 голосов
/ 16 декабря 2008

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

>>> 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 назначен внутри функции, и классифицировал его как локальную переменную. Механизм доступа для локальных пользователей принципиально отличается для глобальных переменных - им статически назначается смещение в таблице переменных фрейма, что означает, что поиск является быстрым индексом, а не более дорогим поиском разборов, как для глобальных. Из-за этого Python читает строку print a как «получить значение локальной переменной« a », хранящейся в слоте 0, и распечатать его», и когда он обнаруживает, что эта переменная все еще не инициализирована, вызывает исключение.

10 голосов
/ 16 декабря 2008

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

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

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

6 голосов
/ 04 июня 2014

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

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 голосов
/ 17 ноября 2009

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

1: docs.python.org / 3.1 / faq / software.html? Highlight = нелокальный # почему-то-я-получаю-unloclocalerror-когда-переменная-имеет-значение

2: docs.python.org / 3.1 / faq / software.html? Highlight = нелокальный # как сделать, как написать функцию с выводом параметров, вызов по ссылке

ссылка первая описывает ошибку 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)
3 голосов
/ 24 января 2009

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

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

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

2 голосов
/ 03 ноября 2016

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()
2 голосов
/ 16 декабря 2008

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

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

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

1 голос
/ 08 декабря 2015

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

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...