PEP 667 – Konsistente Ansichten von Namensräumen
- Autor:
- Mark Shannon <mark at hotpy.org>, Tian Gao <gaogaotiantian at hotmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 30. Juli 2021
- Python-Version:
- 3.13
- Post-History:
- 20. Aug 2021, 22. Feb 2024
- Resolution:
- 25. Apr 2024
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Begründung
- Spezifikation
- Zusammenfassung der Änderungen
- Abwärtskompatibilität
- Implementierung
- Implementierungs-Hinweise
- Vergleich mit PEP 558
- Referenzimplementierung
- Urheberrecht
Zusammenfassung
In frühen Versionen von Python wurden alle Namensräume, ob in Funktionen, Klassen oder Modulen, auf dieselbe Weise implementiert: als Dictionary.
Aus Leistungsgründen wurde die Implementierung von Funktionsnamensräumen geändert. Leider bedeutete dies, dass der Zugriff auf diese Namensräume über locals() und frame.f_locals inkonsistent wurde, und im Laufe der Jahre schlichen sich einige seltsame Fehler ein, als Threads, Generatoren und Coroutinen hinzugefügt wurden.
Dieses PEP schlägt vor, diese Namensräume wieder konsistent zu machen. Änderungen an frame.f_locals werden immer in den zugrunde liegenden Variablen sichtbar sein. Änderungen an lokalen Variablen werden sofort in frame.f_locals sichtbar sein und unabhängig von Threads oder Coroutinen konsistent sein.
Die Funktion locals() verhält sich in Klassen- und Modul-Scopes wie bisher. Für Funktions-Scopes gibt sie einen Momentaufnahme des zugrunde liegenden frame.f_locals zurück, anstatt einen einzelnen gemeinsam genutzten, im Frame-Objekt zwischengespeicherten Dictionary implizit zu aktualisieren.
Motivation
Die Implementierung von locals() und frame.f_locals in Versionen bis einschließlich Python 3.12 ist langsam, inkonsistent und fehlerhaft. Wir wollen sie schneller, konsistenter und vor allem fehlerfrei machen.
Zum Beispiel beim Versuch, lokale Variablen über Frame-Objekte zu manipulieren
class C:
x = 1
sys._getframe().f_locals['x'] = 2
print(x)
gibt 2 aus, aber
def f():
x = 1
sys._getframe().f_locals['x'] = 2
print(x)
f()
gibt 1 aus.
Dies ist inkonsistent und verwirrend. Schlimmer noch, das Verhalten von Python 3.12 kann zu seltsamen Fehlern führen.
Mit diesem PEP würden beide Beispiele 2 ausgeben, da die Änderung auf Funktionsebene direkt in die optimierten lokalen Variablen im Frame geschrieben würde und nicht in einen zwischengespeicherten Dictionary-Schnappschuss.
Es gibt keine kompensierenden Vorteile für das Verhalten von Python 3.12; es ist unzuverlässig und langsam.
Die eingebaute Funktion locals() hat eigene unerwünschte Verhaltensweisen. Weitere Details zu diesen Bedenken finden Sie in PEP 558.
Begründung
Das Attribut frame.f_locals als Schreib-durch-Proxy machen
Die Implementierung von frame.f_locals in Python 3.12 gibt ein Dictionary zurück, das aus dem Array lokaler Variablen zur Laufzeit erstellt wird. Die C-API PyFrame_LocalsToFast() wird dann von Debuggern und Trace-Funktionen aufgerufen, die ihre Änderungen zurück in das Array schreiben möchten (bis Python 3.11 wurde diese API implizit nach jeder Trace-Funktionsaufruf aufgerufen, anstatt explizit von den Trace-Funktionen aufgerufen zu werden).
Dies kann dazu führen, dass das Array und das Dictionary aus dem Gleichgewicht geraten. Schreibvorgänge auf das Frame-Attribut f_locals werden möglicherweise nicht als Änderungen an lokalen Variablen angezeigt, wenn PyFrame_LocalsToFast() nie aufgerufen wird. Schreibvorgänge auf lokale Variablen können verloren gehen, wenn ein Dictionary-Schnappschuss, der vor der Änderung der Variablen erstellt wurde, zurück in den Frame geschrieben wird (da *jede* bekannte Variable, die im Schnappschuss gespeichert ist, zurück in den Frame geschrieben wird, auch wenn sich der im Schnappschuss gespeicherte Wert seit der Erstellung des Schnappschusses geändert hat).
Indem frame.f_locals eine Ansicht auf den zugrunde liegenden Frame zurückgibt, verschwinden diese Probleme. frame.f_locals ist immer mit dem Frame synchron, da es eine Ansicht davon ist, keine Kopie.
Die eingebaute Funktion locals() unabhängige Schnappschüsse zurückgeben lassen
PEP 558 berücksichtigte drei potenzielle Optionen zur Standardisierung des Verhaltens der eingebauten Funktion locals() in optimierten Geltungsbereichen
- das historische Verhalten beibehalten, bei dem jeder Aufruf von
locals()für einen bestimmten Frame einen einzelnen gemeinsam genutzten Schnappschuss der lokalen Variablen aktualisiert - dass
locals()Schreib-durch-Proxy-Instanzen zurückgibt (ähnlich wieframe.f_locals) - dass
locals()wirklich unabhängige Schnappschüsse zurückgibt, sodass Versuche, die Werte lokaler Variablen überexec()zu ändern, *konsistent* ignoriert würden, anstatt unter bestimmten Umständen akzeptiert zu werden
Die letzte Option wurde als diejenige gewählt, die am einfachsten in der Sprachreferenz erklärt und von Benutzern auswendig gelernt werden konnte
- die eingebaute Funktion
locals()liefert eine Momentaufnahme der lokalen Variablen in optimierten Geltungsbereichen und Lese-/Schreibzugriff in anderen Geltungsbereichen; und frame.f_localsbietet Lese-/Schreibzugriff auf die lokalen Variablen in allen Geltungsbereichen, einschließlich optimierter Geltungsbereiche
Dieser Ansatz ermöglicht eine klarere Absicht eines Codeausschnitts, als wenn beide APIs im optimierten Geltungsbereich vollen Lese-/Schreibzugriff gewährt hätten, auch wenn der Schreibzugriff nicht benötigt oder gewünscht wurde. Weitere Details zu dieser Designentscheidung finden Sie in PEP 558, insbesondere im Abschnitt Motivation und Zusätzliche Überlegungen zu exec() und eval() in optimierten Geltungsbereichen.
Dieser Ansatz ist nicht ohne Nachteile, die im nachfolgenden Abschnitt Rückwärtskompatibilität behandelt werden.
Spezifikation
Python API
Das Attribut frame.f_locals
Für Modul- und Klassengeltungsbereiche (einschließlich Aufrufe von exec() und eval()) ist frame.f_locals eine direkte Referenz auf den Namensraum der lokalen Variablen, der bei der Codeausführung verwendet wird.
Für Funktions-Scopes (und andere optimierte Geltungsbereiche) ist es eine Instanz eines neuen Schreib-durch-Proxy-Typs, der direkt das optimierte Array für lokale Variablen im zugrunde liegenden Frame sowie den Inhalt von Zellenreferenzen auf nicht-lokale Variablen ändern kann.
Die Ansichtsobjekte implementieren vollständig die Schnittstelle collections.abc.Mapping und implementieren außerdem die folgenden Operationen für veränderliche Mappings
- Verwendung von Zuweisungen zum Hinzufügen neuer Schlüssel/Wert-Paare
- Verwendung von Zuweisungen zum Aktualisieren des Wertes, der einem Schlüssel zugeordnet ist
- bedingte Zuweisung über die Methode
setdefault() - Massenaktualisierungen über die Methode
update()
Ansichten verschiedener Frames vergleichen sich ungleich, auch wenn sie denselben Inhalt haben.
Alle Schreibvorgänge auf das Mapping f_locals werden sofort in den zugrunde liegenden Variablen sichtbar sein. Alle Änderungen an den zugrunde liegenden Variablen werden sofort im Mapping sichtbar sein.
Das Objekt f_locals ist ein vollständiges Mapping und kann beliebige Schlüssel/Wert-Paare enthalten. Neue Namen, die über die Proxys hinzugefügt werden, werden in einem dedizierten, gemeinsam genutzten Dictionary gespeichert, das im zugrunde liegenden Frame-Objekt liegt (so dass alle Proxy-Instanzen für einen bestimmten Frame auf alle auf diese Weise hinzugefügten Namen zugreifen können).
Zusätzliche Schlüssel (die keinen lokalen Variablen im zugrunde liegenden Frame entsprechen) können wie gewohnt mit del-Anweisungen oder der Methode pop() entfernt werden.
Die Verwendung von del oder der Methode pop() zum Entfernen von Schlüsseln, die lokalen Variablen im zugrunde liegenden Frame entsprechen, wird NICHT unterstützt und führt bei einem Versuch zum Auslösen eines ValueError. Lokale Variablen können nur auf None (oder einen anderen Wert) über den Proxy gesetzt werden, sie können nicht vollständig gelöst werden.
Die Methode clear() ist auf den Schreib-durch-Proxys NICHT implementiert, da unklar ist, wie sie mit der Unfähigkeit umgehen soll, Einträge zu löschen, die lokalen Variablen entsprechen.
Um die Abwärtskompatibilität zu wahren, produzieren Proxy-APIs, die ein neues Mapping erzeugen müssen (wie copy()), reguläre eingebaute dict-Instanzen anstelle von Schreib-durch-Proxy-Instanzen.
Um eine zirkuläre Referenz zwischen Frame-Objekten und den Schreib-durch-Proxys zu vermeiden, gibt jeder Zugriff auf frame.f_locals eine *neue* Schreib-durch-Proxy-Instanz zurück.
Die eingebaute Funktion locals()
locals() wird wie folgt definiert
def locals():
frame = sys._getframe(1)
f_locals = frame.f_locals
if frame._is_optimized(): # Not an actual frame method
f_locals = dict(f_locals)
return f_locals
Für Modul- und Klassengeltungsbereiche (einschließlich Aufrufe von exec() und eval()) gibt locals() weiterhin eine direkte Referenz auf den Namensraum der lokalen Variablen zurück, der bei der Codeausführung verwendet wird (was auch derselbe Wert ist, der von frame.f_locals gemeldet wird).
In optimierten Geltungsbereichen erzeugt jeder Aufruf von locals() eine *unabhängige* Momentaufnahme der lokalen Variablen.
Die eingebauten Funktionen eval() und exec()
Da dieses PEP das Verhalten von locals() ändert, ändert sich auch das Verhalten von eval() und exec().
Unter der Annahme einer Funktion _eval(), die die Aufgabe von eval() mit expliziten Namespace-Argumenten ausführt, kann eval() wie folgt definiert werden
FrameProxyType = type((lambda: sys._getframe().f_locals)())
def eval(expression, /, globals=None, locals=None):
if globals is None:
# No globals -> use calling frame's globals
_calling_frame = sys._getframe(1)
globals = _calling_frame.f_globals
if locals is None:
# No globals or locals -> use calling frame's locals
locals = _calling_frame.f_locals
if isinstance(locals, FrameProxyType):
# Align with locals() builtin in optimized frame
locals = dict(locals)
elif locals is None:
# Globals but no locals -> use same namespace for both
locals = globals
return _eval(expression, globals, locals)
Die angegebene Argumentenbehandlung für exec() wird entsprechend aktualisiert.
(In Python 3.12 und früheren Versionen war es nicht möglich, locals an eval() oder exec() zu übergeben, ohne auch globals zu übergeben, da dies zuvor positionsgebundene Argumente waren. Unabhängig von diesem PEP wurden die eingebauten Funktionen in Python 3.13 aktualisiert, um Schlüsselwortargumente zu akzeptieren)
C API
Ergänzungen zur C-API PyEval
Drei neue C-API-Funktionen werden hinzugefügt
PyObject *PyEval_GetFrameLocals(void)
PyObject *PyEval_GetFrameGlobals(void)
PyObject *PyEval_GetFrameBuiltins(void)
PyEval_GetFrameLocals() ist äquivalent zu: locals(). PyEval_GetFrameGlobals() ist äquivalent zu: globals().
Alle diese Funktionen geben eine neue Referenz zurück.
C-API PyFrame_GetLocals
Die bestehende C-API-Funktion PyFrame_GetLocals(f) ist äquivalent zu f.f_locals. Ihr Rückgabewert ist wie oben für den Zugriff auf f.f_locals beschrieben.
Diese Funktion gibt eine neue Referenz zurück, sodass sie die Erstellung einer neuen Schreib-durch-Proxy-Instanz bei jedem Aufruf in einem optimierten Geltungsbereich ermöglicht.
Veraltete C-APIs
Die folgenden C-API-Funktionen werden als veraltet markiert, da sie geliehene Referenzen zurückgeben
PyEval_GetLocals()
PyEval_GetGlobals()
PyEval_GetBuiltins()
Die folgenden Funktionen (die neue Referenzen zurückgeben) sollten stattdessen verwendet werden
PyEval_GetFrameLocals()
PyEval_GetFrameGlobals()
PyEval_GetFrameBuiltins()
Die folgenden C-API-Funktionen werden zu No-Ops und werden ohne Ersatz als veraltet markiert
PyFrame_FastToLocalsWithError()
PyFrame_FastToLocals()
PyFrame_LocalsToFast()
Alle veralteten Funktionen werden in der Python 3.13-Dokumentation als veraltet gekennzeichnet.
Von diesen Funktionen stellt nur PyEval_GetLocals() eine erhebliche Wartungsbelastung dar. Entsprechend geben Aufrufe von PyEval_GetLocals() in Python 3.14 eine DeprecationWarning aus, mit einem Zielentfernungsdatum von Python 3.16 (zwei Versionen nach Python 3.14). Alternativen werden wie in PyEval_GetLocals Kompatibilität beschrieben empfohlen.
Zusammenfassung der Änderungen
Dieser Abschnitt fasst zusammen, wie sich das festgelegte Verhalten in Python 3.13 und höher vom historischen Verhalten in Python 3.12 und früheren Versionen unterscheidet.
Änderungen der Python API
Änderungen an frame.f_locals
Betrachten Sie das folgende Beispiel
def l():
"Get the locals of caller"
return sys._getframe(1).f_locals
def test():
if 0: y = 1 # Make 'y' a local variable
x = 1
l()['x'] = 2
l()['y'] = 4
l()['z'] = 5
y
print(locals(), x)
Angesichts der Änderungen in diesem PEP gibt test() {'x': 2, 'y': 4, 'z': 5} 2 aus.
In Python 3.12 schlägt dieses Beispiel mit einem UnboundLocalError fehl, da die Definition von y durch l()['y'] = 4 verloren geht.
Wenn die vorletzte Zeile von y in z geändert würde, würde dies immer noch einen NameError auslösen, wie in Python 3.12. Schlüssel, die zu frame.f_locals hinzugefügt werden und keine lexikalischen lokalen Variablen sind, bleiben in frame.f_locals sichtbar, werden aber dynamisch nicht zu lokalen Variablen.
Änderungen an locals()
Betrachten Sie das folgende Beispiel
def f():
exec("x = 1")
print(locals().get("x"))
f()
Angesichts der Änderungen in diesem PEP wird dies *immer* None ausgeben (unabhängig davon, ob x eine definierte lokale Variable in der Funktion ist), da der explizite Aufruf von locals() einen separaten Schnappschuss erzeugt, der von dem implizit im exec()-Aufruf verwendeten Schnappschuss abweicht.
In Python 3.12 würde das exakte Beispiel 1 ausgeben, aber scheinbar nicht zusammenhängende Änderungen an der Definition der involvierten Funktion könnten dazu führen, dass stattdessen None ausgegeben wird (weitere Details zu diesem Thema finden Sie in Zusätzliche Überlegungen zu exec() und eval() in optimierten Geltungsbereichen in PEP 558).
Änderungen an eval() und exec()
Die primäre Änderung, die eval() und exec() betrifft, ist in der Beispielüberschrift "Änderungen an locals()" gezeigt: wiederholte Zugriffe auf locals() in einem optimierten Geltungsbereich teilen keinen gemeinsamen zugrunde liegenden Namensraum mehr implizit.
Änderungen der C-API
Änderung an PyFrame_GetLocals
PyFrame_GetLocals kann in Python 3.12 bereits beliebige Mappings zurückgeben, da exec() und eval() beliebige Mappings als ihr locals-Argument akzeptieren und Metaklassen beliebige Mappings aus ihren __prepare__-Methoden zurückgeben können.
Die Rückgabe eines Frame-Locals-Proxys in optimierten Geltungsbereichen fügt lediglich einen weiteren Fall hinzu, in dem etwas anderes als ein eingebautes Dictionary zurückgegeben wird.
Änderung an PyEval_GetLocals
Die Semantik von PyEval_GetLocals() ist technisch unverändert, ändert sich aber praktisch, da das auf optimierten Frames zwischengespeicherte Dictionary nicht mehr mit anderen Mechanismen für den Zugriff auf die Frame-Locals (eingebaute Funktion locals(), Funktion PyFrame_GetLocals, Frame-Attribute f_locals) gemeinsam genutzt wird.
Abwärtskompatibilität
Kompatibilität der Python API
Die Implementierung, die in Versionen bis einschließlich Python 3.12 verwendet wurde, weist viele Randfälle und Merkwürdigkeiten auf. Code, der diese umgeht, muss möglicherweise geändert werden. Code, der locals() für einfache Vorlagen oder Print-Debugging verwendet, funktioniert weiterhin korrekt. Debugger und andere Tools, die f_locals zur Änderung lokaler Variablen verwenden, funktionieren jetzt korrekt, auch bei Vorhandensein von Threads, Coroutinen und Generatoren.
Kompatibilität von frame.f_locals
Obwohl f.f_locals sich so verhält, als wäre es der Namensraum der Funktion, wird es einige beobachtbare Unterschiede geben. Zum Beispiel wird f.f_locals is f.f_locals für optimierte Frames False sein, da jeder Zugriff auf das Attribut eine neue Schreib-durch-Proxy-Instanz erzeugt.
Jedoch wird f.f_locals == f.f_locals True sein, und alle Änderungen an den zugrunde liegenden Variablen, auf irgendeine Weise, einschließlich der Hinzufügung neuer Variablennamen als Mapping-Schlüssel, werden immer sichtbar sein.
Kompatibilität von locals()
locals() is locals() wird für optimierte Frames False sein, sodass Code wie der folgende einen KeyError auslöst, anstatt 1 zurückzugeben
def f():
locals()["x"] = 1
return locals()["x"]
Um weiterhin zu funktionieren, muss ein solcher Code den zu ändernden Namensraum explizit in einer lokalen Variablen speichern, anstatt sich auf die vorherige implizite Zwischenspeicherung im Frame-Objekt zu verlassen
def f():
ns = {}
ns["x"] = 1
return ns["x"]
Obwohl dies technisch kein formeller Bruch der Abwärtskompatibilität ist (da das Verhalten des Zurückschreibens auf locals() ausdrücklich als undefiniert dokumentiert war), gibt es definitiv Code, der sich auf das bestehende Verhalten verlässt. Entsprechend wird das aktualisierte Verhalten in der Dokumentation ausdrücklich als Änderung vermerkt und im Python 3.13 Porting Guide behandelt.
Um mit einer Kopie von locals() in optimierten Geltungsbereichen auf allen Versionen zu arbeiten, ohne redundante Kopien auf Python 3.13+ zu erstellen, müssen Benutzer eine versionsabhängige Hilfsfunktion definieren, die nur auf Python-Versionen vor Python 3.13 eine explizite Kopie erstellt
if sys.version_info >= (3, 13):
def _ensure_func_snapshot(d):
return d # 3.13+ locals() already returns a snapshot
else:
def _ensure_func_snapshot(d):
return dict(d) # Create snapshot on older versions
def f():
ns = _ensure_func_snapshot(locals())
ns["x"] = 1
return ns
In anderen Geltungsbereichen kann locals().copy() weiterhin bedingungslos aufgerufen werden, ohne redundante Kopien einzuführen.
Auswirkungen auf exec() und eval()
Auch wenn dieses PEP exec() oder eval() nicht direkt modifiziert, wirkt sich die semantische Änderung von locals() auf das Verhalten von exec() und eval() aus, da diese standardmäßig Code im aufrufenden Namensraum ausführen.
Dies stellt ein potenzielles Kompatibilitätsproblem für einige Codes dar, da mit der vorherigen Implementierung, die dasselbe Dictionary zurückgibt, wenn locals() mehrmals im Funktions-Scope aufgerufen wird, der folgende Code aufgrund des implizit geteilten lokalen Variablennamensraums normalerweise funktioniert hat
def f():
exec('a = 0') # equivalent to exec('a = 0', globals(), locals())
exec('print(a)') # equivalent to exec('print(a)', globals(), locals())
print(locals()) # {'a': 0}
# However, print(a) will not work here
f()
Mit den semantischen Änderungen an locals() in diesem PEP schlägt der Aufruf exec('print(a)')' mit NameError fehl, und print(locals()) meldet ein leeres Dictionary, da jede Zeile ihren eigenen separaten Schnappschuss der lokalen Variablen verwendet, anstatt implizit einen einzigen zwischengespeicherten Schnappschuss zu teilen, der im Frame-Objekt gespeichert ist.
Ein gemeinsam genutzter Namensraum für exec()-Aufrufe kann immer noch durch die Verwendung expliziter Namensräume erhalten werden, anstatt sich auf den zuvor implizit geteilten Frame-Namensraum zu verlassen
def f():
ns = {}
exec('a = 0', locals=ns)
exec('print(a)', locals=ns) # 0
f()
Sie können die Variablen im lokalen Geltungsbereich sogar zuverlässig ändern, indem Sie explizit frame.f_locals verwenden, was vorher nicht möglich war (selbst die Verwendung von ctypes zum Aufruf von PyFrame_LocalsToFast unterlag den Zustandinkonsistenzproblemen, die an anderer Stelle in diesem PEP diskutiert werden).
def f():
a = None
exec('a = 0', locals=sys._getframe().f_locals)
print(a) # 0
f()
Das Verhalten von exec() und eval() für Modul- und Klassengeltungsbereiche (einschließlich verschachtelter Aufrufe) wird nicht geändert, da sich das Verhalten von locals() in diesen Bereichen nicht ändert.
Auswirkungen auf andere Codeausführungs-APIs in der Standardbibliothek
pdb und bdb verwenden die API frame.f_locals und können daher lokale Variablen auch in optimierten Frames zuverlässig aktualisieren. Die Implementierung dieses PEP wird mehrere langjährige Fehler in diesen Modulen im Zusammenhang mit Threads, Generatoren, Coroutinen und anderen Mechanismen, die die gleichzeitige Codeausführung ermöglichen, während der Debugger aktiv ist, beheben.
Andere Codeausführungs-APIs in der Standardbibliothek (wie das code-Modul) greifen nicht implizit auf locals() *oder* frame.f_locals zu, aber das Verhalten beim expliziten Übergeben dieser Namensräume ändert sich wie im Rest dieses PEP beschrieben (das Übergeben von locals() in optimierten Geltungsbereichen teilt den Codeausführungsnamensraum nicht mehr implizit über Aufrufe hinweg, das Übergeben von frame.f_locals in optimierten Geltungsbereichen ermöglicht die zuverlässige Modifikation lokaler Variablen und nicht-lokaler Zellreferenzen).
Kompatibilität der C-API
Kompatibilität von PyEval_GetLocals
PyEval_GetLocals() hat historisch nie zwischen der Emulation von locals() oder sys._getframe().f_locals auf Python-Ebene unterschieden, da alle Referenzen auf denselben gemeinsam genutzten Cache der lokalen Variablenbindungen zurückgaben.
Mit diesem PEP ändert sich locals() dahingehend, dass es für optimierte Frames bei jedem Aufruf unabhängige Schnappschüsse zurückgibt, und frame.f_locals (zusammen mit PyFrame_GetLocals) gibt neue Schreib-durch-Proxy-Instanzen zurück.
Da PyEval_GetLocals() eine geliehene Referenz zurückgibt, ist es nicht möglich, seine Semantik an eine dieser Alternativen anzupassen, sodass es die einzige verbleibende API bleibt, die ein gemeinsam genutztes Cache-Dictionary erfordert, das auf dem Frame-Objekt gespeichert ist.
Obwohl dies die Semantik der Funktion technisch unverändert lässt, ermöglicht es keine zusätzlichen Dict-Einträge mehr, die Benutzern anderer APIs sichtbar sind, da diese APIs nicht mehr auf denselben zugrunde liegenden Cache-Dictionary zugreifen.
Wenn PyEval_GetLocals() als Äquivalent zur Python-Funktion locals() verwendet wird, sollte stattdessen PyEval_GetFrameLocals() verwendet werden.
Dieser Code
locals = PyEval_GetLocals();
if (locals == NULL) {
goto error_handler;
}
Py_INCREF(locals);
sollte ersetzt werden durch
// Equivalent to "locals()" in Python code
locals = PyEval_GetFrameLocals();
if (locals == NULL) {
goto error_handler;
}
Wenn PyEval_GetLocals() als Äquivalent zum Aufruf von sys._getframe().f_locals in Python verwendet wird, sollte er durch den Aufruf von PyFrame_GetLocals() auf dem Ergebnis von PyEval_GetFrame() ersetzt werden.
In diesen Fällen sollte der ursprüngliche Code ersetzt werden durch
// Equivalent to "sys._getframe()" in Python code
frame = PyEval_GetFrame();
if (frame == NULL) {
goto error_handler;
}
// Equivalent to "frame.f_locals" in Python code
locals = PyFrame_GetLocals(frame);
frame = NULL; // Minimise visibility of borrowed reference
if (locals == NULL) {
goto error_handler;
}
Auswirkungen auf PEP 709 Inlined Comprehensions
Bei Inlined Comprehensions innerhalb einer Funktion verhält sich locals() derzeit sowohl innerhalb als auch außerhalb der Comprehension gleich, und dies wird sich nicht ändern. Das Verhalten von locals() innerhalb von Funktionen wird sich im Allgemeinen wie im Rest dieses PEP beschrieben ändern.
Bei Inlined Comprehensions auf Modul- oder Klassenebene gibt der Aufruf von locals() innerhalb der Inlined Comprehension bei jedem Aufruf ein neues Dictionary zurück. Dieses PEP wird dazu führen, dass locals() innerhalb einer Funktion ebenfalls bei jedem Aufruf ein neues Dictionary zurückgibt, was die Konsistenz verbessert; Inlined Comprehensions auf Klassen- oder Modul-Ebene werden so erscheinen, als ob die Inlined Comprehension immer noch eine separate Funktion wäre.
Implementierung
Jede Lektüre von frame.f_locals erzeugt ein neues Proxy-Objekt, das den Anschein erweckt, das Mapping von lokalen (einschließlich Zellen- und freien) Variablennamen auf die Werte dieser lokalen Variablen zu sein.
Eine mögliche Implementierung ist unten skizziert. Alle Attribute, die mit einem Unterstrich beginnen, sind unsichtbar und können nicht direkt aufgerufen werden. Sie dienen nur zur Veranschaulichung des vorgeschlagenen Designs.
NULL: Object # NULL is a singleton representing the absence of a value.
class CodeType:
_name_to_offset_mapping_impl: dict | NULL
_cells: frozenset # Set of indexes of cell and free variables
...
def __init__(self, ...):
self._name_to_offset_mapping_impl = NULL
self._variable_names = deduplicate(
self.co_varnames + self.co_cellvars + self.co_freevars
)
...
@property
def _name_to_offset_mapping(self):
"Mapping of names to offsets in local variable array."
if self._name_to_offset_mapping_impl is NULL:
self._name_to_offset_mapping_impl = {
name: index for (index, name) in enumerate(self._variable_names)
}
return self._name_to_offset_mapping_impl
class FrameType:
_locals : array[Object] # The values of the local variables, items may be NULL.
_extra_locals: dict | NULL # Dictionary for storing extra locals not in _locals.
_locals_cache: FrameLocalsProxy | NULL # required to support PyEval_GetLocals()
def __init__(self, ...):
self._extra_locals = NULL
self._locals_cache = NULL
...
@property
def f_locals(self):
return FrameLocalsProxy(self)
class FrameLocalsProxy:
"Implements collections.MutableMapping."
__slots__ = ("_frame", )
def __init__(self, frame:FrameType):
self._frame = frame
def __getitem__(self, name):
f = self._frame
co = f.f_code
if name in co._name_to_offset_mapping:
index = co._name_to_offset_mapping[name]
val = f._locals[index]
if val is NULL:
raise KeyError(name)
if index in co._cells
val = val.cell_contents
if val is NULL:
raise KeyError(name)
return val
else:
if f._extra_locals is NULL:
raise KeyError(name)
return f._extra_locals[name]
def __setitem__(self, name, value):
f = self._frame
co = f.f_code
if name in co._name_to_offset_mapping:
index = co._name_to_offset_mapping[name]
kind = co._local_kinds[index]
if index in co._cells
cell = f._locals[index]
cell.cell_contents = val
else:
f._locals[index] = val
else:
if f._extra_locals is NULL:
f._extra_locals = {}
f._extra_locals[name] = val
def __iter__(self):
f = self._frame
co = f.f_code
yield from iter(f._extra_locals)
for index, name in enumerate(co._variable_names):
val = f._locals[index]
if val is NULL:
continue
if index in co._cells:
val = val.cell_contents
if val is NULL:
continue
yield name
def __contains__(self, item):
f = self._frame
if item in f._extra_locals:
return True
return item in co._variable_names
def __len__(self):
f = self._frame
co = f.f_code
res = 0
for index, _ in enumerate(co._variable_names):
val = f._locals[index]
if val is NULL:
continue
if index in co._cells:
if val.cell_contents is NULL:
continue
res += 1
return len(self._extra_locals) + res
C API
PyEval_GetLocals() wird ungefähr wie folgt implementiert
PyObject *PyEval_GetLocals(void) {
PyFrameObject * = ...; // Get the current frame.
if (frame->_locals_cache == NULL) {
frame->_locals_cache = PyEval_GetFrameLocals();
} else {
PyDict_Update(frame->_locals_cache, PyFrame_GetLocals(frame));
}
return frame->_locals_cache;
}
Wie bei allen Funktionen, die eine geliehene Referenz zurückgeben, muss darauf geachtet werden, dass die Referenz nicht über die Lebensdauer des Objekts hinaus verwendet wird.
Implementierungs-Hinweise
Als das PEP angenommen wurde, wurde vorgeschlagen, dass PyEval_GetLocals eine zwischengespeicherte Instanz des neuen Schreib-durch-Proxys zurückgeben würde, während die Implementierungsskizze angab, dass es weiterhin einen Dictionary-Schnappschuss zurückgeben würde, der im Frame-Instanz zwischengespeichert ist. Diese Diskrepanz wurde bei der Implementierung des PEP festgestellt und vom Steering Council zugunsten der Beibehaltung des Verhaltens von Python 3.12 (Rückgabe eines Dictionary-Schnappschusses, der in der Frame-Instanz zwischengespeichert ist) gelöst. Der PEP-Text wurde entsprechend aktualisiert.
Während der Diskussionen zur Klarstellung der C-API wurde auch deutlich, dass die Begründung für die Änderung von locals(), unabhängige Schnappschüsse in optimierten Geltungsbereichen zurückzugeben, nicht klar war, da sie aus den ursprünglichen Diskussionen zu PEP 558 übernommen wurde, anstatt in diesem PEP unabhängig behandelt zu werden. Der PEP-Text wurde aktualisiert, um diese Änderung besser abzudecken, mit zusätzlichen Aktualisierungen der Abschnitte Spezifikation und Rückwärtskompatibilität, um die Auswirkungen auf Codeausführungs-APIs zu behandeln, die standardmäßig Code im locals()-Namensraum ausführen. Zusätzliche Motivations- und Begründungsdetails wurden ebenfalls in PEP 558 aufgenommen.
In 3.13.0 erlaubten die Schreib-durch-Proxys das Löschen nicht einmal von zusätzlichen Variablen mit del und pop(). Dies wurde anschließend als Kompatibilitätsregression gemeldet und behoben, wie nun in Das Attribut frame.f_locals beschrieben.
Vergleich mit PEP 558
Dieses PEP und PEP 558 teilten ein gemeinsames Ziel: die Semantik von locals() und frame.f_locals() verständlich und ihre Funktionsweise zuverlässig zu machen.
Der Hauptunterschied zwischen diesem PEP und PEP 558 besteht darin, dass PEP 558 versuchte, zusätzliche Variablen in einer vollständigen internen Dictionary-Kopie der lokalen Variablen zu speichern, um die Abwärtskompatibilität mit der Legacy-API PyEval_GetLocals() zu verbessern, während dieses PEP dies nicht tut (es speichert die zusätzlichen lokalen Variablen in einem dedizierten Dictionary, auf das nur über die neuen Frame-Proxy-Objekte zugegriffen wird, und kopiert sie nur bei Bedarf in das gemeinsam genutzte Dict von PyEval_GetLocals()).
PEP 558 gab nicht genau an, wann diese interne Kopie aktualisiert wurde, was das Verhalten von PEP 558 in mehreren Fällen, in denen dieses PEP gut spezifiziert bleibt, unmöglich zu begründen machte.
PEP 558 schlug auch die Einführung einiger zusätzlicher Python-Scope-Introspektionsschnittstellen zur C-API vor, die es Erweiterungsmodulen erleichtern würden zu bestimmen, ob der aktuell aktive Python-Scope optimiert ist oder nicht, und somit ob das locals()-Äquivalent der C-API eine direkte Referenz auf den lokalen Ausführungsnamensraum des Frames oder eine flache Kopie der lokalen Variablen und nicht-lokalen Zellreferenzen des Frames zurückgibt. Ob solche Introspektions-APIs hinzugefügt werden sollen oder nicht, ist unabhängig von den vorgeschlagenen Änderungen an locals() und frame.f_locals, und daher sind keine solchen Vorschläge in diesem PEP enthalten.
PEP 558 wurde letztendlich zurückgezogen zugunsten dieser PEP.
Referenzimplementierung
Die Implementierung befindet sich in der Entwicklung als Entwurf eines Pull Requests auf GitHub.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0667.rst
Zuletzt geändert: 2024-10-27 07:11:46 GMT