In Python wordt de resolutie van de variabele scope statisch uitgevoerd tijdens de compilatiefase in plaats van dynamisch tijdens de uitvoering. Wanneer de CPython-compiler een functiedefinitie tegenkomt, doorloopt het de abstracte syntaxisboom om een symbolentabel te bouwen die elke naam categoriseert als lokaal, globaal of cell-variabele. Als de compiler een bindingoperatie detecteert—zoals toewijzing, vergrote toewijzing of import—voor een naam ergens binnen het functielichaam, markeert het die naam als een lokale variabele voor de gehele scope. Dit ontwerp stelt de virtuele machine in staat om geoptimaliseerde LOAD_FAST-opcodes te gebruiken die werken op een array van vaste grootte in plaats van tragere hash-tabellookups uit te voeren. Deze optimalisatie is fundamenteel voor de prestatie van functieaanroepen in Python, maar introduceert strikte bindingseisen.
Wanneer een naam als lokaal wordt geclassificeerd, genereert de compiler LOAD_FAST bytecode-instructies voor alle leesbewerkingen van die naam. Tijdens de uitvoering probeert LOAD_FAST het objectreferentie op te halen uit de overeenkomstige index in de array van lokale variabelen van het frame. Als de plaats een null-pointer bevat die aangeeft dat er nog geen waarde is toegewezen, werpt de runtime een UnboundLocalError. Dit gebeurt zelfs als er een globale variabele met dezelfde naam bestaat, omdat de compiler opzettelijk heeft vermeden LOAD_GLOBAL uit te geven. De fout geeft expliciet deze statische scoping-beslissing aan, wat het onderscheidt van NameError.
Om dit op te lossen, moet je de compiler expliciet informeren dat de naam naar de globale namespace verwijst door global <variabele_naam> te declareren. Deze declaratie zorgt ervoor dat de compiler overschakelt naar LOAD_GLOBAL en STORE_GLOBAL opcodes, die de naam dynamisch in de globale dictionary van het module opzoeken. Alternatief kun je de code herstructureren om ervoor te zorgen dat alle lokale variabelen bovenaan de functie worden geïnitialiseerd voordat enige voorwaardelijke logica ze leest. Voor geneste scopes dwingt het nonlocal-sleutelwoord de compiler om LOAD_DEREF te gebruiken om toegang te krijgen tot closure-cellen. Deze declaraties wijzigen de bindingbeslissing van de compiler tijdens de compilatietijd, wat het scenario van de onbehaalde lokale variabele voorkomt.
threshold = 100 def analyze(data): # Compiler ziet 'threshold = ...' hieronder, markeert het als lokaal if data > threshold: # Werpt UnboundLocalError op return "high" threshold = 50 # Toewijzing maakt het lokaal # Oplossing met 'global' def analyze_fixed(data): global threshold if data > threshold: # LOAD_GLOBAL slaagt return "high" threshold = 50 # Werk de globale variabele bij
Een data engineeringteam bouwde een ETL-pipeline met Apache Airflow. Ze definieerden een standaardconfiguratiedictionary CONFIG = {"batch_size": 1000} op module-niveau om eenvoudige aanpassing van verwerkingsparameters mogelijk te maken. De hoofdtransformatiefunctie process_batch() controleerde aanvankelijk if len(records) > CONFIG["batch_size"]: om te bepalen of splitsing nodig was. Later in de functie, onder een specifieke voorwaarde, probeerde de code het geheugen te optimaliseren door de batchgrootte te verlagen met CONFIG = {"batch_size": 500}. Dit patroon veroorzaakt onbedoeld een scopeconflict.
Toen de pipeline werd uitgevoerd, crasht het op de eerste regel van de functie met UnboundLocalError: lokale variabele 'CONFIG' verwezen voor toewijzing. De toewijzing aan het einde van de functie zorgde ervoor dat de Python-compiler CONFIG als een lokale variabele voor het gehele functielichaam behandelde. Gevolgelijk gebruikte de vergelijkingsoperatie aan het begin LOAD_FAST om de niet-geïnitialiseerde lokale variabele te benaderen. Deze mislukking stopte de datapipe tijdens een kritieke productie-uitvoering omdat de functie niet kon beginnen met uitvoeren.
Het team overweegde eerst om de lokale heraanwijzing te hernoemen naar local_config, waardoor een nieuwe dictionary werd gemaakt voor de verminderde batchverwerking. Dit zou het schaduwenprobleem volledig vermijden en de globale configuratie onveranderlijk houden. Deze aanpak vereiste echter een refactoring van downstream-code die verwachtte dat de naam CONFIG de huidige limieten weerspiegelde. Het introduceerde mogelijke inconsistenties als de ontwikkelaar vergat de nieuwe variabelenaam in de daaropvolgende logica te gebruiken. De cognitieve overhead van het bijhouden van twee variabelen voor hetzelfde concept maakte deze oplossing minder aantrekkelijk.
Een andere optie was om global CONFIG aan het begin van de functie toe te voegen, waardoor de compiler gedwongen werd alle referenties als globale opzoekingen te behandelen. Hoewel dit de fout zou voorkomen, verwierp het team het omdat het wijzigen van de globale status tijdens een batchproces een gevaarlijk anti-patroon is. Het voorkomt herintrittelijkheid van de functie en compliceert unit-testing aanzienlijk. Bovendien zou het race-omstandigheden creëren als de code ooit over threads werd geparalleliseerd. De bijwerkingen op de globale status op module-niveau werden als onaanvaardbaar beschouwd voor productie-datapipe.
De derde oplossing hield in dat ze de bestaande dictionary ter plaatse mutaten met CONFIG["batch_size"] = 500 in plaats van de variabelenaam zelf opnieuw toe te wijzen. Aangezien deze operatie geen nieuwe binding voor de naam CONFIG creëert, blijft de compiler het als een globale referentie behandelen. Dit voorkomt UnboundLocalError terwijl het de configuratie-update voor volgende aanroepen toestaat. Dit werd als de beste onmiddellijke oplossing beschouwd, hoewel het team van plan was om de configuratie later in een klasse-instantie te refactoren. De mutatie-aanpak behield de bestaande API terwijl het de onmiddellijke crash oploste.
Ze implementeerden de derde oplossing, waarbij de heraanwijzing werd veranderd in een mutatie CONFIG["batch_size"] = 500. De pipeline hernam de uitvoering zonder fouten en de configuratiewijziging werd correct toegepast op volgende batches. Later refactoreerden ze de code om een Pydantic-instellingobject in de functie in te voeren. Dit verwijderde volledig de afhankelijkheid van globale variabelen op module-niveau en maakte de functie puur en testbaar. Het voorval leidde tot een code-review van alle Airflow-operators om soortgelijke schaduwpatern te elimineren.
Waarom zorgt del van een variabele binnen een functie, gevolgd door een poging om deze te lezen, voor een UnboundLocalError in plaats van terug te vallen op de globale scope?
Wanneer je del x uitvoert op een lokale variabele, verwijdert het de referentie uit de f_locals van het frame, maar verandert het de statische classificatie van x als lokaal niet. De compiler genereert nog steeds LOAD_FAST voor volgende leesbewerkingen. Wanneer de interpreter LOAD_FAST uitvoert, vindt hij de plaats leeg en werpt UnboundLocalError in plaats van terug te vallen op globals. Dit bevestigt dat scope-beslissingen op runtime onveranderlijk zijn. Om toegang te krijgen tot een globale x na verwijdering, moet je global x declareren tijdens de compilatietijd.
Hoe vermijden standaardargumentexpressies de valkuil van UnboundLocalError, en wat onthult dit over hun evaluatietiming?
Standaardargumenten worden eenmaal geëvalueerd wanneer de functiedefinitie wordt uitgevoerd in de omringende scope, niet binnen de lokale scope van de functie. Als je schrijft def f(val=CONFIG["key"]):, gebruikt Python LOAD_GLOBAL om CONFIG tijdens de definitietijd op te lossen. Zelfs als de functielichaam later aan CONFIG toewijst, waardoor het lokaal wordt, is de standaard al veilig vastgelegd. Dit onthult dat standaardwaarden de globale scope gebruiken tijdens de definitietijd, gescheiden van de lokale uitvoering van het functielichaam. Hierdoor vermijden standaardwaarden de UnboundLocalError die zou optreden als dezelfde toegang binnen de functielichaam voordat aan de toewijzing zou plaatsvinden.
Waarom komt UnboundLocalError nooit voor in klassenlichamen, en welke bytecodeverschil maakt dit mogelijk?
Klassenlichamen gebruiken LOAD_NAME in plaats van LOAD_FAST voor variabele toegang. LOAD_NAME voert een dynamische opzoeking uit in de klasse-dict, dan de globale dict, dan built-ins. Het gebruikt geen vooraf toegewezen vaste slot, zodat het nooit een "onbehaalde lokale" status tegenkomt. Als een naam wordt referenced voor toewijzing in een klassenlichaam, gaat LOAD_NAME eenvoudig verder om het in de globale scope te vinden. Deze op dict gebaseerde aanpak ruilde de snelheid van functie-lokalen in voor de flexibiliteit die nodig is tijdens de klasseconstructie.