In CPython, de referentie-implementatie van Python, wijkt het gedrag van locals() af afhankelijk van de uitvoeringsscope vanwege optimalisatiestrategieën. Op module-niveau geeft locals() de globale namespace-dictionary zelf terug, wat de autoritatieve opslag voor variabelen is, zodat wijzigingen onmiddellijk in de omgeving worden weerspiegeld. In een functie daarentegen, past CPython een optimalisatie toe genaamd "snelle locals", waarbij variabelen opgeslagen worden in een vaste C-array van PyObject* pointers, geïndexeerd op bytecode in plaats van in een hashtabel. Wanneer locals() binnen een functie wordt aangeroepen, maakt CPython een nieuwe dictionary aan en vult deze door waarden uit deze snelle lokale array te kopiëren, waardoor een tijdelijke snapshot wordt geproduceerd. Gevolg hiervan is dat schrijven naar deze dictionary alleen de efemere mapping bijwerkt, terwijl de onderliggende snelle lokale array ongewijzigd blijft, zodat de functie de oorspronkelijke variabelewaarden blijft gebruiken.
Een ontwikkelingsteam bouwde een dynamisch debuggingtool dat het mogelijk maakte voor ontwikkelaars om tijdelijke hulplokalen in het midden van de scope van een draaiende functie te injecteren via een interface voor externe debugging. De initiële implementatie ving locals() op een breakpoint, injecteerde hulpopjecten in de teruggegeven dictionary, en verwachtte dat de draaiende functie deze hulpmiddelen in de volgende regels zou benaderen.
De eerste aanpak probeerde de dictionary die teruggegeven werd door locals() direct te muteren, opererend onder de veronderstelling dat het een live referentie naar de namespace van de functie was. Voordelen: Het vereiste geen wijzigingen in de functietekens en leek syntactisch eenvoudig. Nadelen: Het faalde stilletjes omdat CPython deze dictionary beschouwt als een alleen-lezen snapshot van de snelle lokale array; wijzigingen werden discarded, waardoor de werkelijke lokale variabelen ongewijzigd bleven.
De tweede strategie hield in dat de tijdelijke staat in globals() werd geïnjecteerd, waarbij de globale namespace als een gedeeld prikbord werd gebruikt. Voordelen: Deze methode behield gegevens in de hele applicatie en was overal toegankelijk zonder argumenten door te geven. Nadelen: Het introduceerde ernstige thread-veiligheidrisico's, vervuilde de globale namespace met tijdelijke debugginggegevens, en schond de principes van encapsulatie door interne staat bloot te stellen aan het hele proces.
De uiteindelijke oplossing refactored de geïnstrumenteerde functies om een expliciete context dictionary-argument te accepteren, waardoor de debugger mutable staat kon doorgeven. Voordelen: Deze benadering is expliciet, thread-veilig, en werkt identiek op CPython, PyPy en Jython, in overeenstemming met het Python-principe dat expliciet beter is dan impliciet. Nadelen: Het vereiste dat de handtekeningen van de doel-functies en aanroeplocaties werden gewijzigd, wat meer initiële refactoring betekende dan de andere benaderingen.
Het team nam de expliciete context doorgeefstrategie over. Dit elimineerde de afhankelijkheid van CPython-specifieke implementatiedetails, voorkwam namespacevervuiling, en resulteerde in een stabiel, cross-platform debugginghulpmiddel.
Waarom gedraagt locals() zich anders binnen een lijst-comprehensie vergeleken met een standaard for-lus op module-niveau?
In Python 3 introduceren lijst-comprehensies hun eigen lokale scope, vergelijkbaar met een geneste functie, om variabele-lekkage van de lusvariabele naar de omringende namespace te voorkomen. Wanneer locals() binnen een comprehensie wordt aangeroepen, geeft het de dictionary voor deze tijdelijke scope terug, niet de omringende functie of module. Bovendien, net zoals in reguliere functies, is deze dictionary een snapshot van snelle locals als de comprehensie als een afzonderlijk codeobject is geïmplementeerd, zodat schrijfsels naar deze dictionary niet persistent zijn. In tegenstelling tot dat, is locals() op module-niveau een alias voor globals(), wat de live module-dictionary is. Dit onderscheid is cruciaal omdat ontwikkelaars vaak aannemen dat comprehensies dezelfde lokale namespace delen als hun omringende blok, wat tot verwarring leidt bij het debuggen of injecteren van variabelen daarin.
Kun je een schrijf-terug naar snelle locals forceren door het frame-object te manipuleren via sys._getframe(), en wat zijn de risico's?
Geavanceerde gebruikers kunnen het huidige uitvoeringsframe benaderen met sys._getframe() en frame.f_locals aanpassen, wat CPython exposeert als een beschrijfbare mapping. In sommige versies kan het toewijzen aan frame.f_locals een schrijf-terug naar de snelle lokale array triggeren met interne API's zoals PyFrame_LocalsToFast, maar dit gedrag is implementatie-afhankelijk, versie-gevoelig en geen onderdeel van de taalspecificatie. De risico's omvatten geheugen-corruptie als de referentietellingen niet correct worden beheerd, inconsistente gedragingen waarbij de optimizer de bijgewerkte waarden negeert omdat ze al zijn gecached in registers of de array, en totale falen in andere Python-implementaties zoals PyPy die helemaal geen snelle lokale array-architectuur gebruiken. Vertrouwen op deze techniek introduceert ongedocumenteerd gedrag en maakt de code onmogelijk te onderhouden over Python-versies.
Hoe beïnvloedt de aanwezigheid van exec() of eval() met expliciete locals de optimalisatie van snelle locals in een functie?
Als een functie-inhoud exec() of eval() oproepen bevat die verwijzen naar de lokale namespace, kan CPython niet garanderen dat variabelen alleen via de geoptimaliseerde snelle lokale array worden benaderd; de uitgevoerde string kan dynamisch variabelen introduceren of verwijderen. Om dit te accommoderen, schakelt de compiler de snelle lokale optimalisatie voor die functie uit, en schakelt over naar het opslaan van alle lokale variabelen in een standaard dictionary die voor elke toegang wordt geraadpleegd. In deze "ongeoptimaliseerde" modus geeft locals() deze werkelijke dictionary terug, waardoor het een live, beschrijfbaar overzicht is waar wijzigingen onmiddellijk persisteren. Dit verklaart waarom code die exec() gebruikt vaak langzamer draait en waarom locals() misschien "correct" lijkt te werken (schrijven toestaan) in zulke functies, terwijl dit in geoptimaliseerde functies niet het geval is.