UnboundLocalError na zmiennej lokalnej po ponownym przypisaniu po pierwszym użyciu

208

Poniższy kod działa zgodnie z oczekiwaniami zarówno w Pythonie 2.5, jak i 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()

Kiedy jednak odkomentuję linię (B) , otrzymuję UnboundLocalError: 'c' not assignedlinię na linii (A) . Wartości ai bsą drukowane poprawnie. To mnie całkowicie zaskoczyło z dwóch powodów:

  1. Dlaczego w linii (A) generowany jest błąd czasu wykonania z powodu późniejszej instrukcji w linii (B) ?

  2. Dlaczego są zmienne ai bdrukowane zgodnie z oczekiwaniami, natomiast cpodnosi błąd?

Jedyne wyjaśnienie, jakie mogę wymyślić, to to, że zmienna lokalnac jest tworzona przez przypisanie c+=1, które ma pierwszeństwo cprzed zmienną „globalną” nawet przed utworzeniem zmiennej lokalnej. Oczywiście nie ma sensu, aby zmienna „kradła” zasięg, zanim on istnieje.

Czy ktoś mógłby wyjaśnić to zachowanie?

tba
źródło

Odpowiedzi:

216

Python traktuje zmienne w funkcjach w różny sposób, w zależności od tego, czy przypisujesz do nich wartości z wnętrza czy z zewnątrz funkcji. Jeśli zmienna jest przypisana do funkcji, jest ona domyślnie traktowana jako zmienna lokalna. Dlatego po odkomentowaniu linii próbujesz odwołać się do zmiennej lokalnej, czanim zostanie do niej przypisana jakakolwiek wartość.

Jeśli chcesz, aby zmienna codnosiła się do globalnego c = 3przypisanego przed funkcją, wstaw

global c

jako pierwszy wiersz funkcji.

Jeśli chodzi o python 3, jest teraz

nonlocal c

którego możesz użyć, aby odnieść się do najbliższego obejmującego zakresu funkcji, który ma czmienną.

rekurencyjny
źródło
3
Dzięki. Szybkie pytanie. Czy to oznacza, że ​​Python decyduje o zakresie każdej zmiennej przed uruchomieniem programu? Przed uruchomieniem funkcji?
tba
7
Kompilator podejmuje decyzję o zmiennym zakresie, która zwykle uruchamia się raz przy pierwszym uruchomieniu programu. Należy jednak pamiętać, że kompilator może również działać później, jeśli w programie znajdują się instrukcje „eval” lub „exec”.
Greg Hewgill,
2
Dobrze dziękuję. Wydaje mi się, że „język interpretowany” nie oznacza tyle, ile myślałem.
tba
1
Ach, to „nielokalne” słowo kluczowe było dokładnie tym, czego szukałem, wydawało się, że Python tego brakuje. Przypuszczalnie to „kaskady” przez każdy obejmujący zakres, który importuje zmienną za pomocą tego słowa kluczowego?
Brendan,
6
@brainfsck: najłatwiej zrozumieć, jeśli rozróżniasz „wyszukiwanie” i „przypisywanie” zmiennej. Wyszukiwanie powraca do wyższego zakresu, jeśli nazwa nie zostanie znaleziona w bieżącym zakresie. Przypisanie odbywa się zawsze w zasięgu lokalnym (chyba że użyjesz globallub nonlocalzmusisz przypisanie globalne lub nielokalne)
Steven,
71

Python jest trochę dziwny, ponieważ przechowuje wszystko w słowniku dla różnych zakresów. Oryginały a, b, c znajdują się w najwyższym zakresie, a więc w tym najwyższym słowniku. Funkcja ma własny słownik. Kiedy dotrzesz do instrukcji print(a)i print(b), w słowniku nie ma nic o tej nazwie, więc Python sprawdza listę i znajduje je w słowniku globalnym.

Teraz dochodzimy do c+=1, co oczywiście jest równoważne c=c+1. Kiedy Python skanuje tę linię, mówi „aha, jest zmienna o nazwie c, wstawię ją do mojego lokalnego słownika zakresu”. Następnie, gdy szuka wartości c dla c po prawej stronie przypisania, znajduje swoją lokalną zmienną o nazwie c , która nie ma jeszcze żadnej wartości, i zgłasza błąd.

Powyższe stwierdzenie global cpo prostu mówi parserowi, że używa on cz zakresu globalnego, a zatem nie potrzebuje nowego.

Powodem, dla którego mówi, że jest problem z linią, którą robi, jest to, że efektywnie szuka nazw, zanim spróbuje wygenerować kod, więc w pewnym sensie nie sądzi, że tak naprawdę robi tę linię. Twierdziłbym, że jest to błąd użyteczności, ale ogólnie dobrą praktyką jest po prostu nauczyć się nie brać wiadomości kompilatora zbyt poważnie.

Jeśli to wygoda, spędziłem prawdopodobnie dzień na kopaniu i eksperymentowaniu z tym samym zagadnieniem, zanim znalazłem coś, co Guido napisał o słownikach, które wyjaśniają wszystko.

Aktualizacja, patrz komentarze:

Nie skanuje kodu dwa razy, ale skanuje kod w dwóch fazach: leksykalnym i parsującym.

Zastanów się, jak działa parsowanie tego wiersza kodu. Lexer odczytuje tekst źródłowy i dzieli go na leksemy, „najmniejsze elementy” gramatyki. Więc kiedy dojdzie do linii

c+=1

dzieli to na coś w rodzaju

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

Analizator składni ostatecznie chce przekształcić to w drzewo analizujące i wykonać je, ale ponieważ jest to zadanie, zanim to zrobi, szuka nazwy c w lokalnym słowniku, nie widzi go i wstawia do słownika, oznaczając jest niezainicjowany. We w pełni skompilowanym języku po prostu wszedłby do tabeli symboli i czekał na analizę, ale ponieważ nie będzie miał luksusu drugiego przejścia, leksykon wykonuje trochę dodatkowej pracy, aby ułatwić życie później. Tylko wtedy widzi OPERATORA, widzi, że zasady mówią „jeśli masz operatora + = lewa strona musiała zostać zainicjowana” i mówi „ups!”

Chodzi o to, że tak naprawdę jeszcze nie zaczął analizować linii . To wszystko dzieje się jako rodzaj przygotowania do rzeczywistej analizy, więc licznik linii nie przejął się do następnej linii. Zatem, gdy sygnalizuje błąd, nadal myśli, że jest w poprzedniej linii.

Jak mówię, można argumentować, że jest to błąd użyteczności, ale w rzeczywistości jest to dość powszechna rzecz. Niektóre kompilatory są bardziej uczciwe i mówią „błąd w linii XXX lub w jej pobliżu”, ale ten nie.

Charlie Martin
źródło
1
OK, dziękuję za odpowiedź; wyjaśniono mi kilka kwestii dotyczących zakresów w pythonie. Jednak nadal nie rozumiem, dlaczego błąd pojawia się na linii (A), a nie na linii (B). Czy Python tworzy słownik o zmiennym zasięgu PRZED uruchomieniem programu?
tba
1
Nie, to na poziomie ekspresji. Dodam do odpowiedzi, nie sądzę, żebym mógł to zmieścić w komentarzu.
Charlie Martin,
2
Uwaga na temat szczegółów implementacji: W CPython zakres lokalny zwykle nie jest traktowany jako dict, jest wewnętrznie tylko tablicą ( locals()zapełni się dictdo zwrócenia, ale zmiany w nim nie tworzą nowych locals). Faza parsowania polega na znalezieniu każdego przypisania do lokalnego i przekształceniu nazwy do pozycji w tej tablicy oraz wykorzystaniu tej pozycji za każdym razem, gdy następuje odwołanie do nazwy. Po wejściu do funkcji locale niebędące argumentami są inicjowane na symbol zastępczy, i UnboundLocalErrordzieje się tak, gdy zmienna jest czytana, a powiązany z nią indeks nadal ma wartość symbolu zastępczego.
ShadowRanger
44

Spojrzenie na demontaż może wyjaśnić, co się dzieje:

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

Jak widać, Kod bajtowy dostępu a jest LOAD_FAST, a dla B LOAD_GLOBAL. Wynika to z tego, że kompilator zidentyfikował, do którego przypisano funkcję a, i sklasyfikował ją jako zmienną lokalną. Mechanizm dostępu dla mieszkańców jest zasadniczo różny dla globałów - statycznie przypisuje im się przesunięcie w tabeli zmiennych ramki, co oznacza, że ​​wyszukiwanie jest szybkim indeksem, a nie droższym wyszukiwaniem dict jak dla globałów. Z tego powodu Python odczytuje print awiersz jako „pobierz wartość zmiennej lokalnej 'a' trzymanej w gnieździe 0 i wydrukuj ją”, a gdy wykryje, że zmienna ta jest nadal niezainicjowana, podnosi wyjątek.

Brian
źródło
10

Python zachowuje się dość interesująco, gdy próbujesz tradycyjnej semantyki zmiennych globalnych. Nie pamiętam szczegółów, ale możesz dobrze odczytać wartość zmiennej zadeklarowanej w zakresie „globalnym”, ale jeśli chcesz ją zmodyfikować, musisz użyć globalsłowa kluczowego. Spróbuj zmienić test()na:

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

Przyczyną tego błędu jest również to, że możesz również zadeklarować nową zmienną wewnątrz tej funkcji o tej samej nazwie co „globalna”, i byłaby ona całkowicie osobna. Tłumacz interpretuje, że próbujesz utworzyć nową zmienną w tym zakresie o nazwie ci zmodyfikować ją w jednej operacji, co nie jest dozwolone w Pythonie, ponieważ ta nowa cnie została zainicjowana.

Mangusta
źródło
Dziękuję za twoją odpowiedź, ale nie sądzę, że wyjaśnia, dlaczego błąd jest generowany w linii (A), gdzie próbuję jedynie wydrukować zmienną. Program nigdy nie dostaje się do linii (B), gdzie próbuje zmodyfikować niezainicjowaną zmienną.
tba
1
Python odczyta, przeanalizuje i przekształci całą funkcję w wewnętrzny kod bajtowy przed uruchomieniem programu, więc fakt, że „zmiana c na zmienną lokalną” dzieje się tekstowo po wydrukowaniu wartości, nie ma znaczenia.
Vatine
6

Najlepszym przykładem, który to wyjaśnia, jest:

bar = 42
def foo():
    print bar
    if False:
        bar = 0

gdy dzwonisz foo(), to również podnosi, UnboundLocalError chociaż nigdy nie dojdziemy do linii bar=0, więc logicznie nie należy nigdy tworzyć zmiennej lokalnej.

Tajemnica tkwi w „ Python jest językiem interpretowanym ”, a deklaracja funkcji foojest interpretowana jako pojedyncza instrukcja (tj. Instrukcja złożona), po prostu głupio interpretuje ją i tworzy zakresy lokalne i globalne. Tak więc barjest rozpoznawany w zasięgu lokalnym przed wykonaniem.

Na więcej przykładów jak to przeczytać ten post: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Ten post zawiera pełny opis i analizy zakresu zmiennych Pythona:

Sahil kalra
źródło
5

Oto dwa linki, które mogą pomóc

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

link pierwszy opisuje błąd UnboundLocalError. Link dwa może pomóc w ponownym zapisaniu funkcji testowej. Na podstawie linku drugiego oryginalny problem można przepisać jako:

>>> 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)
Mcdon
źródło
4

Nie jest to bezpośrednia odpowiedź na twoje pytanie, ale jest ściśle powiązana, ponieważ jest to kolejna problem spowodowany relacją między rozszerzonym przypisaniem a zakresami funkcji.

W większości przypadków myślisz o rozszerzonym przypisaniu ( a += b) jako dokładnie równoważnym z prostym przypisaniem ( a = a + b). Można jednak mieć z tym kłopoty, w jednym rogu. Pozwól mi wyjaśnić:

Sposób, w jaki działa proste przypisanie Pythona, oznacza, że ​​jeśli azostanie przekazany do funkcji (np. func(a)Zwróć uwagę, że Python jest zawsze przekazywany przez odniesienie), to a = a + bnie zmodyfikuje aprzekazanego. Zamiast tego po prostu zmodyfikuje lokalny wskaźnik doa .

Ale jeśli używasz a += b, to czasami jest implementowany jako:

a = a + b

lub czasami (jeśli metoda istnieje) jako:

a.__iadd__(b)

W pierwszym przypadku (o ile anie jest zadeklarowany jako globalny), nie ma żadnych skutków ubocznych poza zasięgiem lokalnym, ponieważ przypisanie do ajest tylko aktualizacją wskaźnika.

W drugim przypadku afaktycznie się zmodyfikuje, więc wszystkie odniesienia do abędą wskazywać na zmodyfikowaną wersję. Pokazuje to następujący kod:

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

Zatem sztuczka polega na uniknięciu rozszerzonego przypisywania argumentów funkcji (staram się używać go tylko do zmiennych lokalnych / pętli). Użyj prostego zadania, a będziesz bezpieczny przed niejednoznacznym zachowaniem.

Alsuren
źródło
2

Interpreter języka Python odczyta funkcję jako kompletną jednostkę. Myślę o tym jak o czytaniu go w dwóch krokach, raz, aby zebrać jego zamknięcie (zmienne lokalne), a potem ponownie, aby przekształcić go w kod bajtowy.

Jak zapewne już wiesz, każda nazwa używana po lewej stronie „=” jest domyślnie zmienną lokalną. Nieraz byłem przyłapany na zmianie dostępu do zmiennej na + = i nagle jest to inna zmienna.

Chciałem również zauważyć, że tak naprawdę nie ma to nic wspólnego z globalnym zasięgiem. Takie samo zachowanie uzyskuje się w przypadku funkcji zagnieżdżonych.

James Hopkin
źródło
2

c+=1 przypisuje c , python zakłada, że ​​przypisane zmienne są lokalne, ale w tym przypadku nie zostało zadeklarowane lokalnie.

Albo użyj globalalbononlocal słowa kluczowego .

nonlocal działa tylko w Pythonie 3, więc jeśli używasz Pythona 2 i nie chcesz, aby twoja zmienna była globalna, możesz użyć obiektu zmiennego:

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()
Colegram
źródło
1

Najlepszym sposobem na osiągnięcie zmiennej klasy jest bezpośredni dostęp według nazwy klasy

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
Harun ERGUL
źródło
0

W Pythonie mamy podobną deklarację dla wszystkich typów zmiennych lokalnych, zmiennych klasowych i zmiennych globalnych. kiedy odwołujesz zmienną globalną z metody, python myśli, że faktycznie odwołujesz się do zmiennej z samej metody, która nie została jeszcze zdefiniowana, i dlatego generuje błąd. Aby odwołać się do zmiennej globalnej, musimy użyć globals () ['nazwa zmiennej'].

w twoim przypadku użyj globals () ['a], globals () [' b '] i globals () [' c '] zamiast odpowiednio a, b i c.

Santosh Kadam
źródło
0

Niepokoi mnie ten sam problem. Za pomocą nonlocali globalmoże rozwiązać problem.
Jednak uwaga wymagana do użycia nonlocal, działa dla zagnieżdżonych funkcji. Jednak na poziomie modułu nie działa. Zobacz przykłady tutaj.

Qinsheng Zhang
źródło