Ошибка области видимости переменной 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
а также b
напечатаны правильно. Это полностью сбило меня с толку по двум причинам:
Почему в строке (A) возникает ошибка времени выполнения из-за более позднего оператора в строке (B)?
Почему переменные
a
а такжеb
печатается как ожидалось, аc
выдает ошибку?
Единственное объяснение, которое я могу придумать, заключается в том, что локальная переменная c
создается назначением c+=1
, который имеет прецедент над "глобальной" переменной c
даже до создания локальной переменной. Конечно, для переменной не имеет смысла "красть" область видимости до того, как она существует.
Может ли кто-нибудь объяснить это поведение?
16 ответов
Python обрабатывает переменные в функциях по-разному в зависимости от того, назначаете ли вы значения из них внутри функции или нет. Если функция содержит какие-либо присвоения переменной, по умолчанию она обрабатывается как локальная переменная. Поэтому, когда вы раскомментируете строку, вы пытаетесь сослаться на локальную переменную до того, как ей будет присвоено какое-либо значение.
Если вы хотите переменную c
ссылаться на глобальный c
положил
global c
в первой строке функции.
Что касается питона 3, то сейчас
nonlocal c
что вы можете использовать для ссылки на ближайшую область действия функции, которая имеет c
переменная.
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 или около нее", но это не так.
Взглянув на разборку, можно уточнить, что происходит:
>>> 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, и распечатать его", и когда она обнаруживает, что эта переменная еще не инициализирована, возникает исключение.
У Python довольно интересное поведение, когда вы используете традиционную семантику глобальных переменных. Я не помню деталей, но вы можете просто прочитать значение переменной, объявленной в "глобальной" области видимости, но если вы хотите изменить ее, вы должны использовать global
ключевое слово. Попробуйте изменить test()
к этому:
def test():
global c
print(a)
print(b)
print(c) # (A)
c+=1 # (B)
Кроме того, причина того, что вы получаете эту ошибку, заключается в том, что вы также можете объявить новую переменную внутри этой функции с тем же именем, что и у "глобальной", и она будет полностью отдельной. Интерпретатор считает, что вы пытаетесь создать новую переменную в этой области c
и изменить все это за одну операцию, что не разрешено в Python, потому что это новое c
не был инициализирован.
Лучший пример, который проясняет это:
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:
Краткое содержание
Python заранее определяет область действия переменной . Если явно не переопределено с помощьюили(в 3.x) ключевые слова, переменные будут распознаны как локальные на основе существования любой операции, которая изменит привязку имени. Это включает в себя обычные присваивания, расширенные присваивания, такие как , различные менее очевидные формы присваивания (конструкция, вложенные функции и классы, операторы...), а также отвязывание (с использованием ). Фактическое выполнение такого кода не имеет значения.
Это также объясняется в документации .
Обсуждение
Вопреки распространенному мнению, Python не является «интерпретируемым» языком в каком-либо осмысленном смысле. (Сейчас они исчезающе редки.) Эталонная реализация Python компилирует код Python почти так же, как Java или C#: он транслируется в коды операций («байт-код») для виртуальной машины, которая затем эмулируется . Другие реализации также должны компилировать код, чтобыSyntaxError
s могут быть обнаружены без фактического запуска кода и для реализации части «служб компиляции» стандартной библиотеки.
Как Python определяет область видимости переменной
Во время компиляции (будь то эталонная реализация или нет) Python следует простым правилам для принятия решений об области действия переменной в функции:
Если функция содержитилиобъявление имени, это имя рассматривается как ссылка на глобальную область или первую объемлющую область, содержащую имя, соответственно.
В противном случае, если он содержит какой-либо синтаксис для изменения привязки (назначения или удаления) имени, даже если код фактически не изменит привязку во время выполнения , имя является локальным .
В противном случае он ссылается либо на первую объемлющую область, содержащую имя, либо на глобальную область в противном случае.
Важно отметить, что область видимости определяется во время компиляции . Сгенерированный байт-код прямо укажет, где искать. Например, в CPython 3.8 есть отдельные коды операций.LOAD_CONST
(константы известны во время компиляции),LOAD_FAST
(местные жители),LOAD_DEREF
(реализовать поиск, просматривая замыкание, которое реализовано как кортеж объектов «ячейки»)LOAD_CLOSURE
(ищите локальную переменную в объекте закрытия, который был создан для вложенной функции), иLOAD_GLOBAL
(ищите что-нибудь либо в глобальном пространстве имен, либо во встроенном пространстве имен).
Для этих имен нет значений по умолчанию . Если они не были назначены до их поиска, возникает ошибка . В частности, для локального поиска,UnboundLocalError
имеет место; это подтипNameError
.
Особые (и не особые) случаи
Здесь есть несколько важных соображений, учитывая, что правило синтаксиса реализуется во время компиляции, без статического анализа :
- Не имеет значения , является ли глобальная переменная встроенной функцией и т. д., а не явно созданной глобальной:
def x(): int = int('1') # `int` is local!
- Не имеет значения , если код никогда не может быть достигнут:
y = 1 def x(): return y # local! if False: y = 0
- Не имеет значения , будет ли присваивание оптимизировано для модификации на месте (например, расширения списка) — концептуально значение все равно присваивается, и это отражается в байт-коде в эталонной реализации как бесполезное переназначение имени для тот же объект:
y = [] def x(): y += [1] # local, even though it would modify `y` in-place with `global`
- Однако имеет значение , если вместо этого мы выполним индексированное/срезовое назначение. (Во время компиляции он преобразуется в другой код операции, который, в свою очередь, вызывает
__setitem__
.)y = [0] def x(): print(y) # global now! No error occurs. y[0] = 1
- Существуют и другие формы присвоения, например
for
петли и с:import sys y = 1 def x(): return y # local! for y in []: pass def z(): print(sys.path) # `sys` is local! import sys
- Еще один распространенный способ вызвать проблемы — это повторно использовать имя модуля в качестве локальной переменной, например:
import random def x(): random = random.choice(['heads', 'tails'])
import
это присваивание, поэтому есть глобальная переменная. Но эта глобальная переменная не является специальной ; он может так же легко быть затенен местнымиrandom
. - Удаление также меняет привязку имени, например:
y = 1 def x(): return y # local! del y
Заинтересованному читателю, использующему эталонную реализацию, предлагается изучить каждый из этих примеров с помощьюdis
стандартный библиотечный модуль.
Охватывающие области и ключевое слово (в 3.x)
Задача работает одинаково, mutatis mutandis, для обоихglobal
и ключевые слова. (В Python 2.x нет
nonlocal
.) В любом случае ключевое слово необходимо присвоить переменной из внешней области, но не обязательно просто искать ее или изменять искомый объект. (Снова:+=
в списке изменяет список, но затем также переназначает имя тому же списку.)
Специальное примечание о глобальных и встроенных функциях
Как видно выше, Python не рассматривает никакие имена как находящиеся «во встроенной области». Вместо этого встроенные функции являются запасным вариантом, используемым при поиске в глобальной области видимости. Присвоение этим переменным будет обновлять только глобальную область, а не встроенную область. Однако в эталонной реализации встроенную область видимости можно изменить: она представлена переменной в глобальном пространстве имен с именем__builtins__
, который содержит объект модуля (встроенные функции реализованы на C, но доступны в виде стандартного библиотечного модуля с именемbuiltins
, который предварительно импортируется и назначается этому глобальному имени). Любопытно, что в отличие от многих других встроенных объектов, этот объект-модуль может изменять свои атрибуты иdel
д. (Все это, насколько я понимаю, должно считаться ненадежной деталью реализации, но так оно работает уже довольно давно.)
Вот две ссылки, которые могут помочь
1: http://docs.python.org/3.1/faq/programming.html?highlight=nonlocal
2: http://docs.python.org/3.1/faq/programming.html?highlight=nonlocal
Первая ссылка описывает ошибку 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)
Это не прямой ответ на ваш вопрос, но он тесно связан, так как это еще одна ошибка, вызванная отношением между расширенным назначением и областями функций.
В большинстве случаев вы склонны думать о расширенном назначении (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
Так что хитрость заключается в том, чтобы избежать расширенного присваивания аргументов функции (я стараюсь использовать его только для локальных переменных / переменных цикла). Используйте простое задание, и вы будете защищены от неоднозначного поведения.
Интерпретатор Python прочитает функцию как законченный модуль. Я думаю о нем как о чтении его в два прохода, один раз, чтобы собрать его замыкание (локальные переменные), а затем снова превратить его в байт-код.
Как я уверен, вы уже знали, что любое имя слева от '=' неявно является локальной переменной. Я не раз поймал меня, изменив доступ к переменной на +=, и вдруг это была другая переменная.
Я также хотел отметить, что это не имеет ничего общего с глобальным охватом. Вы получаете то же самое поведение с вложенными функциями.
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()
Лучший способ получить доступ к переменной класса - это прямой доступ по имени класса.
class Employee:
counter=0
def __init__(self):
Employee.counter+=1
Эта проблема также может возникать, когдаdel
ключевое слово используется в переменной ниже по строке после инициализации, обычно в цикле или условном блоке.
В этом случаеn = num
ниже,n
является локальной переменной и является глобальной переменной:
num = 10
def test():
# ↓ Local variable
n = num
# ↑ Global variable
print(n)
test()
Итак, ошибки нет:
Но в этом случае ниже с обеих сторон локальные переменные иnum
справа еще не определено:
num = 10
def test():
# ↓ Local variable
num = num
# ↑ Local variable not defined yet
print(num)
test()
Итак, ошибка ниже:
UnboundLocalError: ссылка на локальную переменную 'num' перед назначением
Кроме того, даже при удаленииnum = 10
как показано ниже:
# num = 10 # Removed
def test():
# ↓ Local variable
num = num
# ↑ Local variable not defined yet
print(num)
test()
Ниже такая же ошибка:
UnboundLocalError: ссылка на локальную переменную 'num' перед назначением
Итак, чтобы решить ошибку выше, поместитеglobal num
до того, как показано ниже:
num = 10
def test():
global num # Here
num = num
print(num)
test()
Затем ошибка выше решается, как показано ниже:
10
Или определите локальную переменнуюnum = 5
доnum = num
как показано ниже:
num = 10
def test():
num = 5 # Here
num = num
print(num)
test()
Затем ошибка выше решается, как показано ниже:
5
Меня беспокоит та же проблема. С помощьюnonlocal
а также global
может решить проблему.
Однако необходимо обратить внимание на использованиеnonlocal
, он работает для вложенных функций. Однако на уровне модуля это не работает. См. Примеры здесь.
В Python у нас есть аналогичное объявление для всех типов переменных: локальные, переменные класса и глобальные переменные. когда вы ссылаетесь на глобальную переменную из метода, python думает, что вы на самом деле ссылаетесь на переменную из самого метода, который еще не определен, и, следовательно, выдает ошибку. Для ссылки на глобальную переменную мы должны использовать globals()['variableName'].
в вашем случае используйте globals()['a],globals()['b'] и globals()['c'] вместо a,b и c соответственно.
Вы также можете получить это сообщение, если вы определяете переменную с тем же именем, что и метод.
Например:
def teams():
...
def some_other_method():
teams = teams()
Решение состоит в том, чтобы переименовать метод
teams()
на что-то еще вроде
get_teams()
.
Поскольку он используется только локально, сообщение Python вводит в заблуждение!
Вы в конечном итоге с чем-то вроде этого, чтобы обойти это:
def teams():
...
def some_other_method():
teams = get_teams()