UnboundLocalError sulla variabile locale quando riassegnato dopo il primo utilizzo


209

Il codice seguente funziona come previsto in Python 2.5 e 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()

Tuttavia, quando rimuovo il commento alla riga (B) , ottengo un UnboundLocalError: 'c' not assignedalla riga (A) . I valori di ae bsono stampati correttamente. Questo mi ha completamente sconcertato per due motivi:

  1. Perché viene generato un errore di runtime sulla riga (A) a causa di un'istruzione successiva sulla riga (B) ?

  2. Perché sono variabili ae bstampati come previsto, mentre csolleva un errore?

L'unica spiegazione che posso trovare è che una variabile localec viene creata dall'assegnazione c+=1, che ha la precedenza sulla variabile "globale"c ancor prima che venga creata la variabile locale. Naturalmente, non ha senso che una variabile "rubi" l'ambito prima che esista.

Qualcuno potrebbe spiegare questo comportamento?


Risposte:


216

Python tratta le variabili nelle funzioni in modo diverso a seconda che si assegnino loro dei valori dall'interno o dall'esterno della funzione. Se una variabile viene assegnata all'interno di una funzione, viene trattata per impostazione predefinita come una variabile locale. Pertanto, quando si decommenta la riga, si sta tentando di fare riferimento alla variabile locale cprima che un valore sia stato assegnato ad essa.

Se si desidera che la variabile cfaccia riferimento al globale c = 3assegnato prima della funzione, inserire

global c

come prima riga della funzione.

Per quanto riguarda Python 3, ora c'è

nonlocal c

che è possibile utilizzare per fare riferimento all'ambito della funzione di chiusura più vicino che ha una cvariabile.


3
Grazie. Domanda veloce. Ciò implica che Python decide l'ambito di ciascuna variabile prima di eseguire un programma? Prima di eseguire una funzione?
martedì

7
La decisione sull'ambito variabile viene presa dal compilatore, che normalmente viene eseguito una volta al primo avvio del programma. Tuttavia, vale la pena ricordare che il compilatore potrebbe essere eseguito anche in seguito se nel programma sono presenti istruzioni "eval" o "exec".
Greg Hewgill,

2
Okay grazie. Immagino che il "linguaggio interpretato" non implichi tanto quanto avevo pensato.
martedì

1
Ah, quella parola chiave "non locale" era esattamente quello che stavo cercando, sembrava che a Python mancasse questo. Presumibilmente questa "cascata" attraverso ogni ambito racchiuso che importa la variabile usando questa parola chiave?
Brendan,

6
@brainfsck: è più facile capire se si fa una distinzione tra "cercare" e "assegnare" una variabile. La ricerca ricade in un ambito superiore se il nome non viene trovato nell'ambito corrente. L'assegnazione viene sempre eseguita nell'ambito locale (a meno che non si utilizzi globalo nonlocalper forzare l'assegnazione globale o non locale)
Steven

71

Python è un po 'strano in quanto mantiene tutto in un dizionario per i vari ambiti. Gli originali a, b, c si trovano nell'ambito più in alto e quindi in quel dizionario più in alto. La funzione ha il suo dizionario. Quando raggiungi le istruzioni print(a)e print(b), non c'è nulla con quel nome nel dizionario, quindi Python cerca l'elenco e le trova nel dizionario globale.

Ora arriviamo a c+=1, che è, ovviamente, equivalente a c=c+1. Quando Python analizza quella linea, dice "aha, c'è una variabile chiamata c, la inserirò nel mio dizionario di ambito locale". Quindi, quando cerca un valore per c per la c sul lato destro dell'assegnazione, trova la sua variabile locale denominata c , che non ha ancora alcun valore e quindi genera l'errore.

L'affermazione di global ccui sopra dice semplicemente al parser che utilizza l' cambito globale e quindi non ne ha bisogno di uno nuovo.

Il motivo per cui dice che c'è un problema sulla linea che fa è perché sta effettivamente cercando i nomi prima di provare a generare codice, e quindi in un certo senso non pensa che stia ancora facendo quella linea. Direi che si tratta di un bug di usabilità, ma in genere è una buona pratica imparare a non prendere troppo sul serio i messaggi di un compilatore .

Se è di conforto, ho probabilmente trascorso una giornata a scavare e sperimentare questo stesso problema prima di trovare qualcosa che Guido aveva scritto sui dizionari che spiegavano tutto.

Aggiorna, vedi commenti:

Non esegue la scansione del codice due volte, ma esegue la scansione del codice in due fasi, il lexing e l'analisi.

Considera come funziona l'analisi di questa riga di codice. Il lexer legge il testo sorgente e lo suddivide in lessici, i "componenti più piccoli" della grammatica. Quindi quando colpisce la linea

c+=1

lo spezza in qualcosa del genere

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

Il parser alla fine vuole trasformarlo in un albero di analisi ed eseguirlo, ma poiché è un compito, prima di farlo, cerca il nome c nel dizionario locale, non lo vede e lo inserisce nel dizionario, contrassegnandolo come non inizializzato. In un linguaggio completamente compilato, andrebbe semplicemente nella tabella dei simboli e aspetterebbe l'analisi, ma dal momento che NON avrà il lusso di un secondo passaggio, il lexer fa un piccolo lavoro extra per rendere la vita più facile in seguito. Solo, poi vede l'OPERATORE, vede che le regole dicono "se hai un operatore + = il lato sinistro deve essere stato inizializzato" e dice "whoops!"

Il punto qui è che non ha ancora veramente iniziato l'analisi della linea . Tutto ciò sta accadendo in una sorta di preparazione all'analisi effettiva, quindi il contatore della linea non è passato alla riga successiva. Pertanto, quando segnala l'errore, pensa ancora alla riga precedente.

Come ho detto, potresti obiettare che si tratta di un bug di usabilità, ma in realtà è una cosa abbastanza comune. Alcuni compilatori ne sono più onesti e dicono "errore sulla linea XXX o intorno", ma questo non lo fa.


1
Va bene grazie per la tua risposta; mi ha chiarito alcune cose sugli ambiti in Python. Tuttavia, non capisco ancora perché l'errore sia stato generato alla riga (A) anziché alla riga (B). Python crea il suo dizionario a portata variabile PRIMA di eseguire il programma?
martedì

1
No, è a livello di espressione. Aggiungerò alla risposta, non credo di poterlo inserire in un commento.
Charlie Martin,

2
Nota sui dettagli di implementazione: in CPython, l'ambito locale di solito non è gestito come a dict, è internamente solo un array ( locals()popolerà un dictper restituirlo, ma le modifiche ad esso non ne creano di nuove locals). La fase di analisi sta trovando ogni assegnazione in un locale e convertendo da nome a posizione in quell'array e usando quella posizione ogni volta che si fa riferimento al nome. Quando si accede alla funzione, i locali senza argomento vengono inizializzati su un segnaposto e si UnboundLocalErrorverificano quando viene letta una variabile e l'indice associato ha ancora il valore di segnaposto.
ShadowRanger

44

Dare un'occhiata allo smontaggio può chiarire cosa sta succedendo:

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

Come si può vedere, il bytecode per l'accesso a è LOAD_FAST, e per la B, LOAD_GLOBAL. Questo perché il compilatore ha identificato che a è assegnato a all'interno della funzione e lo ha classificato come variabile locale. Il meccanismo di accesso per i locali è sostanzialmente diverso per i globali: viene staticamente assegnato un offset nella tabella delle variabili del frame, il che significa che la ricerca è un indice rapido, piuttosto che la ricerca di dict più costosa come per i globali. Per questo motivo, Python sta leggendo la print ariga come "ottieni il valore della variabile locale 'a' contenuto nello slot 0 e stampalo", e quando rileva che questa variabile è ancora non inizializzata, solleva un'eccezione.


10

Python ha un comportamento piuttosto interessante quando provi la semantica variabile globale tradizionale. Non ricordo i dettagli, ma puoi leggere bene il valore di una variabile dichiarata nell'ambito 'globale', ma se vuoi modificarlo, devi usare la globalparola chiave. Prova a cambiare test()in questo modo:

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

Inoltre, il motivo per cui stai ricevendo questo errore è perché puoi anche dichiarare una nuova variabile all'interno di quella funzione con lo stesso nome di una 'globale', e sarebbe completamente separata. L'interprete ritiene che si stia tentando di creare una nuova variabile in questo ambito chiamata ce modificarla in un'unica operazione, cosa non consentita in Python perché questa nuova cnon è stata inizializzata.


Grazie per la tua risposta, ma non credo che spieghi perché l'errore viene generato alla riga (A), dove sto semplicemente cercando di stampare una variabile. Il programma non arriva mai alla riga (B) dove sta cercando di modificare una variabile non inizializzata.
martedì

1
Python leggerà, analizzerà e trasformerà l'intera funzione in bytecode interno prima che inizi a eseguire il programma, quindi il fatto che la "svolta c in variabile locale" avvenga testualmente dopo la stampa del valore non è, per così dire, importante.
Vatine,

6

Il miglior esempio che lo chiarisce è:

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

quando si chiama foo() , questo aumenta anche UnboundLocalErrorse non arriveremo mai alla linea bar=0, quindi non dovrebbe mai essere creata una variabile locale logicamente.

Il mistero sta nel " Python is a Interpreted Language " e la dichiarazione della funzione fooè interpretata come una singola istruzione (cioè un'istruzione composta), la interpreta semplicemente in modo stupido e crea ambiti locali e globali. Quindi barviene riconosciuto nell'ambito locale prima dell'esecuzione.

Per altri esempi come questo Leggi questo post: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Questo post fornisce una descrizione completa e analisi dello scoping Python delle variabili:


5

Ecco due link che possono aiutare

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

il collegamento uno descrive l'errore UnboundLocalError. Il collegamento due può aiutare a riscrivere la funzione di test. Sulla base del collegamento due, il problema originale potrebbe essere riscritto come:

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

Questa non è una risposta diretta alla tua domanda, ma è strettamente correlata, in quanto è un altro gotcha causato dalla relazione tra assegnazione aumentata e ambiti di funzione.

Nella maggior parte dei casi, si tende a pensare all'assegnazione aumentata ( a += b) come esattamente equivalente all'assegnazione semplice ( a = a + b). Tuttavia, è possibile avere qualche problema con questo, in un caso d'angolo. Lasciatemi spiegare:

Il modo in cui funziona la semplice assegnazione di Python significa che se aviene passato in una funzione (come func(a); nota che Python è sempre pass-by-reference), allora a = a + bnon modificherà aciò che viene passato. Invece, modificherà semplicemente il puntatore locale su a.

Ma se lo usi a += b, a volte viene implementato come:

a = a + b

o talvolta (se il metodo esiste) come:

a.__iadd__(b)

Nel primo caso (purché anon sia dichiarato globale), non ci sono effetti collaterali al di fuori dell'ambito locale, poiché l'assegnazione a aè solo un aggiornamento del puntatore.

Nel secondo caso, asi modificherà effettivamente, quindi tutti i riferimenti a afaranno riferimento alla versione modificata. Ciò è dimostrato dal seguente codice:

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

Quindi il trucco è evitare l'assegnazione aumentata sugli argomenti delle funzioni (cerco di usarla solo per le variabili local / loop). Usa un compito semplice e sarai al sicuro da comportamenti ambigui.


2

L'interprete Python leggerà una funzione come unità completa. Lo considero come leggerlo in due passaggi, una volta per raccogliere la sua chiusura (le variabili locali), poi di nuovo per trasformarlo in codice byte.

Come sono sicuro che già sapevi, qualsiasi nome usato a sinistra di un '=' è implicitamente una variabile locale. Più di una volta sono stato sorpreso cambiando un accesso variabile a + = ed è improvvisamente una variabile diversa.

Volevo anche sottolineare che non ha nulla a che fare specificamente con l'ambito globale. Ottieni lo stesso comportamento con le funzioni nidificate.


2

c+=1assegna c, python presuppone che le variabili assegnate siano locali, ma in questo caso non è stato dichiarato localmente.

Utilizzare le parole chiave globalo nonlocal.

nonlocal funziona solo in Python 3, quindi se stai usando Python 2 e non vuoi rendere globale la tua variabile, puoi usare un oggetto mutabile:

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

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

test()

1

Il modo migliore per raggiungere la variabile di classe è accedere direttamente al nome della classe

class Employee:
    counter=0

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

0

In Python abbiamo una dichiarazione simile per tutti i tipi di variabili locali, variabili di classe e variabili globali. quando si fa riferimento alla variabile globale dal metodo, Python pensa che si stia effettivamente facendo riferimento alla variabile dal metodo stesso che non è ancora definito e quindi genera errore. Per fare riferimento alla variabile globale dobbiamo usare globals () ['variabileName'].

nel tuo caso usa globals () ['a], globals () [' b '] e globals () [' c '] invece di a, b e c rispettivamente.


0

Lo stesso problema mi dà fastidio. Utilizzando nonlocale in globalgrado di risolvere il problema.
Tuttavia, attenzione necessaria per l'utilizzo di nonlocal, funziona per le funzioni nidificate. Tuttavia, a livello di modulo, non funziona. Vedi esempi qui.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.