UnboundLocalError für lokale Variable bei Neuzuweisung nach der ersten Verwendung

208

Der folgende Code funktioniert wie erwartet in Python 2.5 und 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()

Wenn ich jedoch Zeile (B) auskommentiere, erhalte ich eine UnboundLocalError: 'c' not assignedZeile (A) . Die Werte von aund bwerden korrekt gedruckt. Das hat mich aus zwei Gründen völlig verblüfft:

  1. Warum wird in Zeile (A) aufgrund einer späteren Anweisung in Zeile (B) ein Laufzeitfehler ausgegeben ?

  2. Warum sind Variablen aund bwie erwartet gedruckt, während cein Fehler auslöst?

Die einzige Erklärung, die ich finden kann, ist, dass durch die Zuweisung eine lokale Variable cerstellt wird c+=1, die Vorrang vor der "globalen" Variablen hat, cnoch bevor die lokale Variable erstellt wird. Natürlich ist es nicht sinnvoll, dass eine Variable den Bereich "stiehlt", bevor er existiert.

Könnte jemand bitte dieses Verhalten erklären?

tba
quelle

Antworten:

215

Python behandelt Variablen in Funktionen unterschiedlich, je nachdem, ob Sie ihnen Werte innerhalb oder außerhalb der Funktion zuweisen. Wenn eine Variable innerhalb einer Funktion zugewiesen ist, wird sie standardmäßig als lokale Variable behandelt. Wenn Sie die Zeile auskommentieren, versuchen Sie daher, auf die lokale Variable zu verweisen, cbevor ihr ein Wert zugewiesen wurde.

Wenn Sie möchten, dass sich die Variable cauf die c = 3vor der Funktion zugewiesene globale Variable bezieht , setzen Sie

global c

als erste Zeile der Funktion.

Python 3 gibt es jetzt

nonlocal c

Damit können Sie auf den nächsten umschließenden Funktionsbereich verweisen, der eine cVariable enthält.

rekursiv
quelle
3
Vielen Dank. Schnelle Frage. Bedeutet dies, dass Python den Umfang jeder Variablen entscheidet, bevor ein Programm ausgeführt wird? Vor dem Ausführen einer Funktion?
tba
7
Die Entscheidung über den variablen Bereich wird vom Compiler getroffen, der normalerweise einmal ausgeführt wird, wenn Sie das Programm zum ersten Mal starten. Beachten Sie jedoch, dass der Compiler möglicherweise auch später ausgeführt wird, wenn Ihr Programm Anweisungen "eval" oder "exec" enthält.
Greg Hewgill
2
Okay, danke. Ich denke, "interpretierte Sprache" bedeutet nicht ganz so viel, wie ich gedacht hatte.
tba
1
Ah, dieses 'nichtlokale' Schlüsselwort war genau das, wonach ich gesucht habe. Es schien, als würde Python dies fehlen. Vermutlich kaskadiert dies durch jeden umschließenden Bereich, der die Variable mit diesem Schlüsselwort importiert?
Brendan
6
@brainfsck: Es ist am einfachsten zu verstehen, wenn Sie zwischen "Nachschlagen" und "Zuweisen" einer Variablen unterscheiden. Die Suche greift auf einen höheren Bereich zurück, wenn der Name im aktuellen Bereich nicht gefunden wird. Die Zuweisung erfolgt immer im lokalen Bereich (es sei denn, Sie verwenden globaloder nonlocalerzwingen eine globale oder nicht lokale Zuweisung)
Steven
71

Python ist insofern etwas seltsam, als es alles in einem Wörterbuch für die verschiedenen Bereiche enthält. Das Original a, b, c befindet sich im obersten Bereich und damit in diesem obersten Wörterbuch. Die Funktion hat ein eigenes Wörterbuch. Wenn Sie die Anweisungen print(a)und erreichen print(b), enthält das Wörterbuch nichts mit diesem Namen. Python schlägt die Liste nach und findet sie im globalen Wörterbuch.

Jetzt kommen wir zu c+=1, was natürlich gleichbedeutend ist mit c=c+1. Wenn Python diese Zeile durchsucht, heißt es: "Aha, es gibt eine Variable mit dem Namen c. Ich werde sie in mein lokales Bereichswörterbuch aufnehmen." Wenn es dann auf der rechten Seite der Zuweisung nach einem Wert für c für c sucht, findet es seine lokale Variable mit dem Namen c , die noch keinen Wert hat, und löst so den Fehler aus.

Die global coben erwähnte Anweisung teilt dem Parser lediglich mit, dass er den caus dem globalen Bereich verwendet und daher keinen neuen benötigt.

Der Grund, warum es ein Problem in der Zeile gibt, ist, dass es effektiv nach den Namen sucht, bevor es versucht, Code zu generieren, und daher in gewissem Sinne nicht glaubt, dass es diese Zeile noch wirklich tut. Ich würde behaupten, dass dies ein Usability-Fehler ist, aber es ist im Allgemeinen eine gute Praxis, einfach zu lernen, die Nachrichten eines Compilers nicht zu ernst zu nehmen.

Wenn es ein Trost ist, habe ich wahrscheinlich einen Tag damit verbracht, mit demselben Thema zu graben und zu experimentieren, bevor ich etwas fand, das Guido über die Wörterbücher geschrieben hatte, die alles erklärten.

Update, siehe Kommentare:

Der Code wird nicht zweimal gescannt, aber der Code wird in zwei Phasen gescannt: Lexen und Parsen.

Überlegen Sie, wie die Analyse dieser Codezeile funktioniert. Der Lexer liest den Quelltext und zerlegt ihn in Lexeme, die "kleinsten Komponenten" der Grammatik. Also, wenn es die Linie trifft

c+=1

es zerlegt es in so etwas wie

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

Der Parser möchte dies schließlich in einen Analysebaum umwandeln und ausführen. Da es sich jedoch um eine Zuweisung handelt, sucht er zuvor im lokalen Wörterbuch nach dem Namen c, sieht ihn nicht und fügt ihn in das Wörterbuch ein und markiert ihn es als nicht initialisiert. In einer vollständig kompilierten Sprache würde es einfach in die Symboltabelle gehen und auf die Analyse warten, aber da es nicht den Luxus eines zweiten Durchgangs hat, macht der Lexer ein wenig zusätzliche Arbeit, um das Leben später einfacher zu machen. Nur dann sieht es den OPERATOR, sieht, dass die Regeln sagen "wenn Sie einen Operator haben + = die linke Seite muss initialisiert worden sein" und sagen "whoops!"

Der Punkt hier ist, dass es noch nicht wirklich mit dem Parsen der Linie begonnen hat . Dies alles geschieht in gewisser Weise vor der eigentlichen Analyse, sodass der Zeilenzähler nicht zur nächsten Zeile vorgerückt ist. Wenn es also den Fehler signalisiert, denkt es immer noch, dass es in der vorherigen Zeile steht.

Wie gesagt, man könnte argumentieren, dass es sich um einen Usability-Fehler handelt, aber es ist tatsächlich eine ziemlich häufige Sache. Einige Compiler sind ehrlicher und sagen "Fehler in oder um Zeile XXX", aber dieser nicht.

Charlie Martin
quelle
1
Okay, danke für deine Antwort. Es hat mir einige Dinge über Bereiche in Python geklärt. Ich verstehe jedoch immer noch nicht, warum der Fehler eher in Zeile (A) als in Zeile (B) auftritt. Erstellt Python sein Wörterbuch mit variablem Gültigkeitsbereich, BEVOR das Programm ausgeführt wird?
tba
1
Nein, es ist auf der Ausdrucksebene. Ich werde der Antwort hinzufügen, ich glaube nicht, dass ich dies in einen Kommentar einfügen kann.
Charlie Martin
2
Hinweis zu Implementierungsdetails: In CPython wird der lokale Bereich normalerweise nicht als dict, sondern intern nur als Array behandelt ( locals()füllt ein, dictum zurückzugeben, aber Änderungen daran erstellen keine neuen locals). In der Analysephase wird jede Zuordnung zu einem lokalen Objekt gefunden, von Name zu Position in diesem Array konvertiert und diese Position verwendet, wenn auf den Namen verwiesen wird. Beim Aufrufen der Funktion werden Nichtargument-Lokale mit einem Platzhalter initialisiert. Dies UnboundLocalErrorgeschieht, wenn eine Variable gelesen wird und der zugehörige Index weiterhin den Platzhalterwert hat.
ShadowRanger
44

Ein Blick auf die Demontage kann verdeutlichen, was passiert:

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

Wie Sie sehen können, ist der Bytecode für den Zugriff auf a LOAD_FASTund für b , LOAD_GLOBAL. Dies liegt daran, dass der Compiler festgestellt hat, dass a innerhalb der Funktion zugewiesen ist, und es als lokale Variable klassifiziert hat. Der Zugriffsmechanismus für Einheimische unterscheidet sich grundlegend für Globals - ihnen wird statisch ein Offset in der Variablentabelle des Frames zugewiesen, was bedeutet, dass die Suche ein schneller Index ist und nicht die teurere Diktatsuche wie für Globals. Aus diesem Grund liest Python die print aZeile als "Wert der lokalen Variablen 'a' in Steckplatz 0 abrufen und drucken" und löst eine Ausnahme aus, wenn festgestellt wird, dass diese Variable noch nicht initialisiert ist.

Brian
quelle
10

Python hat ein ziemlich interessantes Verhalten, wenn Sie die traditionelle globale Variablensemantik ausprobieren. Ich erinnere mich nicht an die Details, aber Sie können den Wert einer Variablen, die im 'globalen' Bereich deklariert ist, gut lesen, aber wenn Sie ihn ändern möchten, müssen Sie das globalSchlüsselwort verwenden. Versuchen Sie test(), dies zu ändern :

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

Der Grund, warum Sie diesen Fehler erhalten, ist, dass Sie in dieser Funktion auch eine neue Variable mit demselben Namen wie eine 'globale' deklarieren können, die vollständig getrennt wäre. Der Interpreter glaubt, dass Sie versuchen, eine neue Variable in diesem Bereich aufzurufen cund alles in einer Operation zu ändern, was in Python nicht zulässig ist, da diese neue cnicht initialisiert wurde.

Mungo
quelle
Vielen Dank für Ihre Antwort, aber ich glaube nicht, dass dies erklärt, warum der Fehler in Zeile (A) ausgegeben wird, in der ich lediglich versuche, eine Variable zu drucken. Das Programm gelangt nie zu Zeile (B), in der es versucht, eine nicht initialisierte Variable zu ändern.
tba
1
Python liest, analysiert und wandelt die gesamte Funktion in internen Bytecode um, bevor das Programm ausgeführt wird. Die Tatsache, dass "C in lokale Variable verwandeln" nach dem Drucken des Werts in Textform erfolgt, spielt also keine Rolle.
Vatine
6

Das beste Beispiel, das es deutlich macht, ist:

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

Beim Aufruf foo()wird dies ebenfalls ausgelöst, UnboundLocalError obwohl wir niemals die Zeile erreichen bar=0werden. Daher sollte niemals eine logisch lokale Variable erstellt werden.

Das Geheimnis liegt in " Python ist eine interpretierte Sprache " und die Deklaration der Funktion foowird als einzelne Anweisung (dh als zusammengesetzte Anweisung) interpretiert. Sie interpretiert sie nur dumm und erstellt lokale und globale Bereiche. Wird baralso vor der Ausführung im lokalen Bereich erkannt.

Für weitere Beispiele wie diese lesen diesen Beitrag: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Dieser Beitrag enthält eine vollständige Beschreibung und Analyse des Python-Scoping von Variablen:

Sahil Kalra
quelle
5

Hier sind zwei Links, die helfen können

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 eins beschreibt den Fehler UnboundLocalError. Link zwei kann beim Umschreiben Ihrer Testfunktion helfen. Basierend auf Link zwei könnte das ursprüngliche Problem wie folgt umgeschrieben werden:

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

Dies ist keine direkte Antwort auf Ihre Frage, aber sie ist eng miteinander verbunden, da es sich um ein weiteres Problem handelt, das durch die Beziehung zwischen erweiterter Zuweisung und Funktionsumfang verursacht wird.

In den meisten Fällen neigen Sie dazu, Augmented Assignment ( a += b) als genau gleichwertig mit Simple Assignment ( a = a + b) zu betrachten. In einem Eckfall kann es jedoch zu Problemen kommen. Lassen Sie mich erklären:

Die Art und Weise, wie die einfache Zuweisung von Python funktioniert, bedeutet, dass, wenn Python aan eine Funktion übergeben wird (z. B. func(a)beachten Sie, dass Python immer als Referenz übergeben wird), a = a + bdie übergebene Funktion nicht geändert awird. Stattdessen wird lediglich der lokale Zeiger auf geändert a.

Wenn Sie jedoch verwenden a += b, wird es manchmal wie folgt implementiert:

a = a + b

oder manchmal (wenn die Methode existiert) als:

a.__iadd__(b)

Im ersten Fall (solange anicht als global deklariert) treten keine Nebenwirkungen außerhalb des lokalen Bereichs auf, da die Zuweisung an anur eine Zeigeraktualisierung ist.

Im zweiten Fall aändert sich tatsächlich selbst, sodass alle Verweise auf aauf die geänderte Version verweisen. Dies wird durch den folgenden Code demonstriert:

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

Der Trick besteht also darin, eine erweiterte Zuweisung von Funktionsargumenten zu vermeiden (ich versuche, sie nur für lokale / Schleifenvariablen zu verwenden). Verwenden Sie eine einfache Zuordnung, und Sie sind vor mehrdeutigem Verhalten geschützt.

alsuren
quelle
2

Der Python-Interpreter liest eine Funktion als vollständige Einheit. Ich stelle es mir vor, es in zwei Durchgängen zu lesen, einmal, um seinen Abschluss (die lokalen Variablen) zu erfassen, und dann wieder, um es in Bytecode umzuwandeln.

Wie Sie sicher bereits wussten, ist jeder Name, der links von einem '=' verwendet wird, implizit eine lokale Variable. Mehr als einmal wurde ich durch das Ändern eines Variablenzugriffs auf ein + = überrascht, und es ist plötzlich eine andere Variable.

Ich wollte auch darauf hinweisen, dass dies nicht wirklich etwas mit dem globalen Geltungsbereich zu tun hat. Sie erhalten das gleiche Verhalten mit verschachtelten Funktionen.

James Hopkin
quelle
2

c+=1Zuweisen c, Python geht davon aus, dass zugewiesene Variablen lokal sind, aber in diesem Fall wurde es nicht lokal deklariert.

Verwenden Sie entweder die Schlüsselwörter globaloder nonlocal.

nonlocal funktioniert nur in Python 3. Wenn Sie also Python 2 verwenden und Ihre Variable nicht global machen möchten, können Sie ein veränderbares Objekt verwenden:

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

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

test()
Colegram
quelle
1

Der beste Weg, um eine Klassenvariable zu erreichen, ist der direkte Zugriff über den Klassennamen

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
Harun ERGUL
quelle
0

In Python haben wir eine ähnliche Deklaration für alle Arten von Variablen, lokale, Klassenvariablen und globale Variablen. Wenn Sie eine globale Variable von der Methode referenzieren, denkt Python, dass Sie tatsächlich eine Variable von der Methode selbst referenzieren, die noch nicht definiert ist, und wirft daher einen Fehler aus. Um auf eine globale Variable zu verweisen, müssen wir globals () ['variableName'] verwenden.

Verwenden Sie in Ihrem Fall globals () ['a], globals () [' b '] und globals () [' c '] anstelle von a, b bzw. c.

Santosh Kadam
quelle
0

Das gleiche Problem stört mich. Verwenden nonlocalund globalkann das Problem lösen.
Die für die Verwendung von erforderliche Aufmerksamkeit nonlocalfunktioniert jedoch für verschachtelte Funktionen. In einer Modulebene funktioniert dies jedoch nicht. Siehe Beispiele hier.

Qinsheng Zhang
quelle