UnboundLocalError op lokale variabele wanneer opnieuw toegewezen na eerste gebruik

De volgende code werkt zoals verwacht in zowel Python 2.5 als 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()

Als ik echter het commentaar op regel (B)verwijder, krijg ik een UnboundLocalError: 'c' not assignedop regel (A). De waarden van aen bworden correct afgedrukt. Dit heeft me volledig verbijsterd om twee redenen:

  1. Waarom wordt er een runtime-fout gegenereerd op regel (A)vanwege een latere verklaring op regel (B)?

  2. Waarom worden variabelen aen bafgedrukt zoals verwacht, terwijl ceen fout geeft?

De enige verklaring die ik kan bedenken is dat een lokalevariabele cwordt gemaakt door de toewijzing c+=1, die een precedent heeft over de “algemene” variabele czelfs voordat de lokale variabele is gemaakt. Het heeft natuurlijk geen zin voor een variabele om de scope te “stelen” voordat deze bestaat.

Kan iemand dit gedrag uitleggen?


Antwoord 1, autoriteit 100%

Python behandelt variabelen in functies anders, afhankelijk van of u er waarden aan toekent van binnen of buiten de functie. Als een variabele binnen een functie wordt toegewezen, wordt deze standaard behandeld als een lokale variabele. Daarom probeer je, wanneer je het commentaar op de regel verwijdert, te verwijzen naar de lokale variabele cvoordat er een waarde aan is toegewezen.

Als u wilt dat de variabele cverwijst naar de globale c = 3die vóór de functie is toegewezen, plaatst u

global c

als de eerste regel van de functie.

Wat python 3 betreft, er is nu

nonlocal c

die u kunt gebruiken om te verwijzen naar het dichtstbijzijnde omsluitende functiebereik met een c-variabele.


Antwoord 2, autoriteit 33%

Python is een beetje raar omdat het alles in een woordenboek bijhoudt voor de verschillende scopes. De originele a,b,c zitten in het bovenste bereik en dus in dat bovenste woordenboek. De functie heeft zijn eigen woordenboek. Wanneer u de print(a)– en print(b)-instructies bereikt, staat er niets met die naam in het woordenboek, dus Python zoekt de lijst op en vindt ze in de globale woordenboek.

Nu komen we bij c+=1, wat natuurlijk gelijk staat aan c=c+1. Wanneer Python die regel scant, staat er “aha, er is een variabele met de naam c, ik zal het in mijn lokale scope-woordenboek plaatsen.” Als het vervolgens op zoek gaat naar een waarde voor c voor de c aan de rechterkant van de toewijzing, vindt het zijn lokale variabele met de naam c, die nog geen waarde heeft, en geeft dus de fout.

De bovenstaande instructie global cvertelt de parser eenvoudig dat deze de cuit de globale scope gebruikt en dus geen nieuwe nodig heeft.

De reden dat het zegt dat er een probleem is met de regel die het doet, is omdat het effectief naar de namen zoekt voordat het code probeert te genereren, en dus in zekere zin denkt dat het die regel nog niet echt doet. Ik zou zeggen dat dit een usability bug is, maar het is over het algemeen een goede gewoonte om gewoon te leren de berichten van een compiler niet teserieus te nemen.

Als het een troost kan zijn, ik heb waarschijnlijk een dag besteed aan het graven en experimenteren met hetzelfde probleem voordat ik iets vond dat Guido had geschreven over de woordenboeken die alles verklaarden.

Update, zie opmerkingen:

Het scant de code niet twee keer, maar het scant de code wel in twee fasen, lexing en parsing.

Bedenk hoe het ontleden van deze regel code werkt. De lexer leest de brontekst en verdeelt deze in lexemen, de “kleinste componenten” van de grammatica. Dus wanneer het de regel raakt

c+=1

het verdeelt het in iets als

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

De parser wil er uiteindelijk een ontledingsboom van maken en uitvoeren, maar aangezien het een toewijzing is, zoekt hij eerst naar de naam c in het lokale woordenboek, ziet deze niet en voegt deze in de woordenboek en markeert het als niet-geïnitialiseerd. In een volledig gecompileerde taal zou het gewoon naar de symbolentabel gaan en wachten op de ontleding, maar aangezien het NIET de luxe van een tweede pas zal hebben, doet de lexer wat extra werk om het leven later gemakkelijker te maken. Alleen, dan ziet hij de OPERATOR, ziet dat de regels zeggen “als je een operator += hebt, moet de linkerkant zijn geïnitialiseerd” en zegt “oeps!”

Het punt hier is dat het nog niet echt is begonnen met het ontleden van de regel. Dit gebeurt allemaal als voorbereiding op de eigenlijke ontleding, dus de regelteller is niet doorgegaan naar de volgende regel. Dus als het de fout signaleert, denkt het nog steeds dat het op de vorige regel staat.

Zoals ik al zei, je zou kunnen stellen dat het een usability-bug is, maar het is eigenlijk een vrij algemeen iets. Sommige compilers zijn er eerlijker over en zeggen “fout op of rond regel XXX”, maar deze niet.


Antwoord 3, autoriteit 19%

Een blik op de demontage kan verduidelijken wat er aan de hand is:

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

Zoals je kunt zien, is de bytecode voor toegang tot a LOAD_FAST, en voor b LOAD_GLOBAL. Dit komt omdat de compiler heeft vastgesteld dat a is toegewezen aan binnen de functie en deze heeft geclassificeerd als een lokale variabele. Het toegangsmechanisme voor locals is fundamenteel anders voor globals – ze krijgen statisch een offset toegewezen in de variabelentabel van het frame, wat betekent dat opzoeken een snelle index is, in plaats van de duurdere dict-lookup zoals voor globals. Hierdoor leest Python de regel print aals “haal de waarde van de lokale variabele ‘a’ die in slot 0 wordt vastgehouden en print deze”, en wanneer het detecteert dat deze variabele nog niet is geïnitialiseerd, maakt een uitzondering.


Antwoord 4, autoriteit 4%

Python vertoont nogal interessant gedrag als je de traditionele semantiek van globale variabelen probeert. Ik herinner me de details niet, maar je kunt de waarde van een variabele die is gedeclareerd in ‘algemeen’ bereik prima lezen, maar als je deze wilt wijzigen, moet je het trefwoord globalgebruiken. Probeer test()in dit te veranderen:

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

De reden dat je deze fout krijgt is ook omdat je ook een nieuwe variabele binnen die functie kunt declareren met dezelfde naam als een ‘algemene’, en het zou volledig gescheiden zijn. De interpreter denkt dat je een nieuwe variabele in dit bereik probeert te maken met de naam cen deze in één bewerking aan te passen, wat niet is toegestaan ​​in Python omdat deze nieuwe cniet geïnitialiseerd.


Antwoord 5, autoriteit 3%

Het beste voorbeeld dat het duidelijk maakt is:

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

bij het aanroepen van foo(), dit verhoogt ookUnboundLocalErrorhoewel we nooit de regel bar=0, dus logischerwijs mag er nooit een lokale variabele worden aangemaakt.

Het mysterie ligt in “Python is een geïnterpreteerde taal” en de declaratie van de functie foowordt geïnterpreteerd als een enkele instructie (dwz een samengestelde instructie), interpreteert het dom en creëert lokale en globale scopes. Dus barwordt herkend in lokale scope voor uitvoering.

Voor meer voorbeeldenzoals deze Lees dit bericht: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Dit bericht geeft een volledige beschrijving en analyses van de Python-scoping van variabelen:


Antwoord 6, autoriteit 2%

Hier zijn twee links die kunnen helpen

1: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-ik-getting-an-unboundlocalerror-when-the-variable-has -een-waarde

2: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call -op referentie

link één beschrijft de fout UnboundLocalError. Link twee kan helpen bij het herschrijven van je testfunctie. Op basis van link twee zou het oorspronkelijke probleem kunnen worden herschreven als:

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

Antwoord 7

Dit is geen direct antwoord op uw vraag, maar het is nauw verwant, aangezien het een andere valkuil is die wordt veroorzaakt door de relatie tussen uitgebreide toewijzing en functiebereiken.

In de meeste gevallen heb je de neiging om augmented toewijzing (a += b) te beschouwen als exact gelijk aan eenvoudige toewijzing (a = a + b). Het is echter mogelijk om in een hoekgeval hiermee in de problemen te komen. Laat het me uitleggen:

De manier waarop de eenvoudige toewijzing van Python werkt, betekent dat als awordt doorgegeven aan een functie (zoals func(a); merk op dat Python altijd pass-by-reference is) , dan zal a = a + bde adie wordt doorgegeven niet wijzigen. In plaats daarvan zal het alleen de lokale aanwijzer wijzigen in a.

Maar als u a += bgebruikt, wordt het soms geïmplementeerd als:

a = a + b

of soms (als de methode bestaat) als:

a.__iadd__(b)

In het eerste geval (zolang aniet globaal wordt verklaard), zijn er geen neveneffecten buiten de lokale scope, aangezien de toewijzing aan aslechts een aanwijzer update.

In het tweede geval zal azichzelf daadwerkelijk wijzigen, dus alle verwijzingen naar averwijzen naar de gewijzigde versie. Dit wordt aangetoond door de volgende code:

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

Dus de truc is om uitgebreide toewijzing van functieargumenten te vermijden (ik probeer het alleen te gebruiken voor local/loop-variabelen). Gebruik een eenvoudige toewijzing en u bent gevrijwaard van dubbelzinnig gedrag.


Antwoord 8

De Python-interpreter leest een functie als een complete eenheid. Ik zie het als het lezen in twee stappen, een keer om de afsluiting (de lokale variabelen) te verzamelen, dan weer om het in byte-code te veranderen.

Zoals ik zeker weet dat je al bewust was, is elke naam die links van A ‘=’ wordt gebruikt impliciet een lokale variabele. Meer dan eens dat ik heb gepakt door een variabele toegang te veranderen tot A + = en het is plotseling een andere variabele.

Ik wilde ook wijzen dat het niet echt iets te maken heeft met de wereldwijde scope specifiek. Je krijgt hetzelfde gedrag met geneste functies.

Other episodes