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

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

Jednak, gdy odkomentuję linię (B) , dostaję UnboundLocalError: 'c' not assigned w linii (A) . Wartości a i b są drukowane poprawnie. To mnie kompletnie zaskoczyło z dwóch powodów:

  1. Dlaczego błąd runtime jest wyrzucany w linii (A) z powodu późniejszego Oświadczenia on line (B)?

  2. Dlaczego zmienne a i b wydrukowane zgodnie z oczekiwaniami, podczas gdy c rodzi błąd?

Jedyne wyjaśnienie, jakie mogę wymyślić, to to, że lokalna zmienna c jest tworzona przez przypisanie c+=1, które ma precedens nad "globalną" zmienną c jeszcze przed utworzeniem lokalnej zmiennej. Oczywiście, nie ma sensu, aby zmienna "kradła" zakres zanim ona istnieje.

Czy ktoś mógłby wyjaśnić to zachowanie?
Author: Carcigenicate, 2008-12-16

12 answers

Python traktuje zmienne w funkcjach w różny sposób w zależności od tego, czy przypisujesz im wartości z wewnątrz czy z zewnątrz funkcji. Jeśli zmienna jest przypisana w funkcji, jest domyślnie traktowana jako zmienna lokalna. Dlatego, gdy odkomentujesz linię, próbujesz odwołać się do zmiennej lokalnej c przed przypisaniem do niej jakiejkolwiek wartości.

Jeśli chcesz, aby zmienna c odnosiła się do globalnej c = 3 przypisanej przed funkcją, umieść

global c

Jako pierwsza linia funkcji.

Co do Pythona 3, jest teraz

nonlocal c

, którego można użyć do odwołania się do najbliższego zakresu funkcji, który ma zmienną c.

 235
Author: recursive,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-11-25 20:00:22

Python jest trochę dziwny, ponieważ trzyma wszystko w słowniku dla różnych zakresów. Oryginalne a, b, c znajdują się w górnym zakresie i tak w górnym słowniku. Funkcja posiada własny słownik. Gdy dotrzesz do instrukcji print(a) i print(b), w słowniku nie ma nic o takiej nazwie, więc Python wyszukuje listę i znajduje ją w globalnym słowniku.

Teraz przejdziemy do c+=1, co jest oczywiście równoważne c=c+1. Kiedy Python skanuje tę linię, mówi "aha, jest zmienna o nazwie c, umieszczę ją w moim lokalnym słowniku zakresu."Wtedy, gdy szuka wartości dla c dla C po prawej stronie przypisania, znajduje swoją zmienną lokalną o nazwie c , która nie ma jeszcze wartości, a więc wyrzuca błąd.

Oświadczenie global c wspomniane powyżej po prostu mówi parserowi, że używa c z zakresu globalnego i dlatego nie potrzebuje nowego.

Powodem, dla którego mówi, że jest problem, jest to, że skutecznie szuka nazw, zanim spróbuje wygenerować kod, więc w pewnym sensie nie myśli, że naprawdę robi tę linię jeszcze. Argumentowałbym, że jest to błąd użyteczności, ale ogólnie dobrą praktyką jest po prostu nauczyć się, aby nie traktować wiadomości kompilatora zbyt poważnie.

Jeśli to jakaś pociecha, spędziłem prawdopodobnie dzień na kopaniu i eksperymentowaniu z tym samym problemem, zanim znalazłem coś, co Guido napisał o słownikach, które wyjaśniały Wszystko.

Aktualizacja, Zobacz komentarze:

Nie skanuje kodu dwa razy, ale skanuje kod w dwóch fazach, lexing i parsing.

Rozważ, jak działa parse tej linii kodu. Lexer odczytuje tekst źródłowy i dzieli go na leksemy, "najmniejsze składniki" gramatyki. Więc kiedy uderzy w linię

c+=1

Rozbija go na coś w rodzaju

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

Parser w końcu chce przekształcić to w drzewo parsowania i wykonać je, ale ponieważ jest to zadanie, zanim to zrobi, szuka nazwy c w lokalnym słowniku, nie widzi jej i wstawia ją do słownika, oznaczając jako niezainicjowaną. W pełni skompilowanym języku, to po prostu przejść do tabeli symboli i czekać na parse, ale ponieważ nie będzie miał Luksus drugiego przejścia, lexer robi trochę dodatkowej pracy, aby ułatwić życie później. Tylko, wtedy widzi operatora, widzi, że zasady mówią "jeśli masz operatora += lewa strona musi być zainicjowany "i mówi" UPS!"

Chodzi o to, że jeszcze nie zaczął parsowania linii. To wszystko dzieje się tak, jakby przygotowując się do aktualnego parsowania, więc licznik linii nie awansował do następnej linii. Tak więc kiedy sygnalizuje błąd, nadal myśli jego na poprzedniej linii.

Jak mówię, można by argumentować, że to błąd użyteczności, ale to dość powszechna rzecz. Niektóre Kompilatory są bardziej szczere i mówią " błąd na linii lub wokół niej XXX", ale ten nie.

 77
Author: Charlie Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-10 07:09:58

Przyjrzenie się demontażowi 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 widzisz, bajtowy kod dostępu a to LOAD_FAST, A Dla b LOAD_GLOBAL. Dzieje się tak dlatego, że kompilator zidentyfikował, że a jest przypisane do funkcji i zaklasyfikował ją jako zmienną lokalną. Mechanizm dostępu dla lokalnych jest zasadniczo inny dla globali-są statycznie przypisane przesunięcie w tabeli zmiennych Ramki, co oznacza, że lookup jest szybkim indeksem, a nie droższym / align = "left" / Z tego powodu Python odczytuje linię print a jako "get the value of local variable 'a' held in slot 0, and print it", a kiedy wykryje, że ta zmienna jest nadal niezinicjalizowana, wyświetla wyjątek.

 48
Author: Brian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-16 09:49:28

Python ma dość interesujące zachowanie, jeśli spróbujesz tradycyjnej semantyki zmiennych globalnych. Nie pamiętam szczegółów, ale możesz odczytać wartość zmiennej zadeklarowanej w 'globalnym' zasięgu, ale jeśli chcesz ją zmodyfikować, musisz użyć słowa kluczowego global. Spróbuj zmienić test() na to:

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

Również powodem, dla którego otrzymujesz ten błąd, jest to, że możesz również zadeklarować nową zmienną wewnątrz tej funkcji o tej samej nazwie, co "globalna" i byłaby całkowicie osobno. Interpreter uważa, że próbujesz utworzyć nową zmienną w tym zakresie o nazwie c i zmodyfikować ją w ramach jednej operacji, co nie jest dozwolone w Pythonie, ponieważ ta nowa c nie została zainicjowana.

 11
Author: Mongoose,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-10 06:53:57

Najlepszym przykładem, który daje do zrozumienia jest:

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

Wywołując foo(), to również podnosi UnboundLocalError chociaż nigdy nie dotrzemy do linii bar=0, więc logicznie lokalna zmienna nigdy nie powinna być tworzona.

Tajemnica tkwi w "Python jest językiem interpretowanym ", a deklaracja funkcji foo jest interpretowana jako pojedyncze oświadczenie (tj. złożone oświadczenie), po prostu interpretuje ją głupio i tworzy lokalne i globalne zakresy. Więc bar jest rozpoznawana w zasięg lokalny przed wykonaniem.

For more examples like this Read this 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:

 6
Author: Sahil kalra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-04 10:39:21

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 two może pomóc w ponownym napisaniu funkcji testowej. W oparciu o link two, oryginał 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)
 5
Author: mcdon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-13 04:00:56

Nie jest to bezpośrednia odpowiedź na twoje pytanie, ale jest ona ściśle ze sobą powiązana, ponieważ jest to kolejna przesłanka spowodowana relacją między przydziałem rozszerzonym a zakresem funkcji.

W większości przypadków masz tendencję do myślenia o rozszerzonym zadaniu (a += b) jako dokładnie równoważnym prostemu zadaniu (a = a + b). Możliwe jest, aby dostać się w pewne kłopoty z tym jednak, w jednym przypadku rogu. Pozwól mi wyjaśnić:

Sposób działania prostego przypisania Pythona oznacza, że jeśli {[5] } zostanie przekazana do funkcja (podobnie jak func(a); zauważ, że Python jest zawsze referencją pass-by-reference), wtedy a = a + b nie zmodyfikuje a, która jest przekazywana. Zamiast tego zmieni lokalny wskaźnik na a.

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

a = a + b

Lub czasami (jeśli metoda istnieje) jako:

a.__iadd__(b)

W pierwszym przypadku (o ile a nie jest zadeklarowana jako globalna), nie ma efektów ubocznych poza lokalnym zakresem, ponieważ przypisanie do a jest tylko wskaźnikiem aktualizacja.

W drugim przypadku a faktycznie się zmodyfikuje, więc wszystkie odniesienia do a będą wskazywały na zmodyfikowaną wersję. Wynika to z następującego kodu:

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

Więc sztuką jest unikanie rozszerzonego przypisywania argumentów funkcji(staram się używać go tylko dla zmiennych local/loop). Użyj prostego przypisania, a będziesz bezpieczny od niejednoznacznego zachowania.

 4
Author: alsuren,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-10 07:07:03

Interpreter Pythona odczyta funkcję jako kompletną jednostkę. Myślę o tym jak o odczytaniu go w dwóch przebiegach, raz, aby zebrać jego zamknięcie( zmienne lokalne), a następnie ponownie, aby przekształcić go w kod bajtowy.

Jak zapewne już wiesz, każda nazwa użyta po lewej stronie ' = ' jest w domyśle zmienną lokalną. Nie raz zostałem złapany przez zmianę zmiennej access na += i nagle jest inna zmienna.

Chciałem również zaznaczyć, że to nie jest naprawdę nic do zrobienia w szczególności o zasięgu globalnym. Takie samo zachowanie uzyskuje się w przypadku funkcji zagnieżdżonych.

 2
Author: James Hopkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-16 08:58:10

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

Użyj słów kluczowych global lub nonlocal.

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ć mutowalnego obiektu:

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

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

test()
 2
Author: Colegram,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-03 18:52:39

Najlepszym sposobem dotarcia do zmiennej klasy jest bezpośredni dostęp przez nazwę klasy

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
 1
Author: Harun ERGUL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-08 10:09:47

W Pythonie mamy podobną deklarację dla wszystkich typów zmiennych: zmiennych lokalnych, klasowych i globalnych. Gdy odnosisz się do zmiennej globalnej z metody, Python myśli, że faktycznie odnosisz się do zmiennej z samej metody, która nie jest jeszcze zdefiniowana, a zatem wyświetla błąd.

Aby odnieść się do zmiennej globalnej musimy użyć globals()['variableName'].

W Twoim przypadku użyj globals()['a], globals()['b'] i globals()['c'] zamiast odpowiednio a, b I c.

 0
Author: Santosh Kadam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-07-29 13:27:26

Ten sam problem mnie martwi. Użycie nonlocal i global może rozwiązać problem.
Jednak uwaga jest potrzebna do użycia nonlocal, działa ON dla funkcji zagnieżdżonych. Jednak na poziomie modułu nie działa. Zobacz przykłady tutaj.

 0
Author: Qinsheng Zhang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-12-06 21:16:23