UnboundLocalError для локальної змінної, коли їх перепризначають після першого використання


208

У 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ще до створення локальної змінної. Зрозуміло, немає сенсу для змінної "красти" сферу, перш ніж вона існує.

Чи могла б хто-небудь пояснити цю поведінку?

Відповіді:


216

Python розглядає змінні у функціях по-різному, залежно від того, призначаєте ви їм значення зсередини або ззовні функції. Якщо змінна призначена в межах функції, вона за замовчуванням розглядається як локальна змінна. Тому, коли ви коментуєте рядок, ви намагаєтеся посилатися на локальну змінну cдо того, як їй буде призначено якесь значення.

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

global c

як перший рядок функції.

Що стосується python 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
Гаразд дякую за вашу відповідь; це роз’яснило мені деякі речі щодо областей в пітоні. Однак я все ще не розумію, чому помилка виникає в рядку (A), а не в рядку (B). Чи створює Python свій словник із змінною сферою дії перед тим, як запустити програму?
tba

1
Ні, це на рівні вираження. Я додам до відповіді, я не думаю, що я можу це вмістити в коментарі.
Чарлі Мартін

2
Примітка щодо деталей реалізації: У CPython локальний обсяг зазвичай не обробляється як a dict, це внутрішньо просто масив ( locals()буде заповнено a dictдля повернення, але зміни в ньому не створюють нових locals). Фаза розбору - це пошук кожного призначення локальному та перетворення з імені в позицію в цьому масиві та використання цього положення щоразу, коли ім'я посилається. Після вступу до функції неаргументовані локалі ініціалізуються до заповнювача, і вони UnboundLocalErrorтрапляються, коли змінна зчитується, а пов'язаний з нею індекс все ще має значення заповнення.
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

Як бачите, байт-код для доступу до a є LOAD_FAST, а для b - 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 на локальну змінну" відбувається текстуально після друку значення, як ніби має значення.
Ватін

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:


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)

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

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


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

1

Найкращий спосіб досягти змінної класу - це прямий доступ до назви класу

class Employee:
    counter=0

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

0

У python у нас є аналогічне оголошення для всіх типів змінних локальних, змінних класів та глобальних змінних. коли ви посилаєте глобальну змінну від методу, python вважає, що ви насправді посилаєтесь на змінну від самого методу, який ще не визначений, а значить, помилка кидка. Для позначення глобальної змінної ми повинні використовувати globals () ['variaName'].

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


0

Ця ж проблема мене турбує. Використання nonlocalта globalможе вирішити проблему.
Однак увага, необхідна для використання nonlocal, працює для вкладених функцій. Однак на рівні модуля це не працює. Дивіться приклади тут.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.