PEP 558 – Definierte Semantik für locals()
- Autor:
- Alyssa Coghlan <ncoghlan at gmail.com>
- BDFL-Delegate:
- Nathaniel J. Smith
- Discussions-To:
- Python-Dev Liste
- Status:
- Zurückgezogen
- Typ:
- Standards Track
- Erstellt:
- 08-Sep-2017
- Python-Version:
- 3.13
- Post-History:
- 08-Sep-2017, 22-Mai-2019, 30-Mai-2019, 30-Dez-2019, 18-Jul-2021, 26-Aug-2021
Inhaltsverzeichnis
- Rücknahme eines PEP
- Zusammenfassung
- Motivation
- Vorschlag
- CPython Implementierungsänderungen
- Zusammenfassung der vorgeschlagenen implementierungsspezifischen Änderungen
- Bereitstellung der aktualisierten Python-Level-Semantik
- Lösung der Probleme mit dem Trace-Modusverhalten
- Details zur Implementierung des schnellen Locals-Proxys
- Änderungen an der stabilen C API/ABI
- Änderungen an der öffentlichen CPython C API
- Reduzierung des Laufzeit-Overheads von Trace-Hooks
- Begründung und Design-Diskussion
- Änderung von
locals(), um unabhängige Schnappschüsse im Funktions-Scope zurückzugeben - Beibehaltung von
locals()als Schnappschuss im Funktions-Scope - Was passiert mit den Standardargumenten für
eval()undexec()? - Zusätzliche Überlegungen zu
eval()undexec()in optimierten Scopes - Beibehaltung des internen Frame-Wert-Caches
- Änderung der Semantik der Frame-API im regulären Betrieb
- Fortgesetzte Unterstützung für die Speicherung zusätzlicher Daten auf optimierten Frames
- Historische Semantik im Funktions-Scope
- Vorschlag mehrerer Ergänzungen zur stabilen C API/ABI
- Vergleich mit PEP 667
- Änderung von
- Implementierung
- Danksagungen
- Referenzen
- Urheberrecht
Rücknahme eines PEP
Im Dezember 2021 konvergierten diese PEP und PEP 667 auf eine gemeinsame Definition der vorgeschlagenen Änderungen an der Python-Level-Semantik des locals() Builtins (wie im PEP-Text unten dokumentiert), wobei die einzigen verbleibenden Unterschiede in den vorgeschlagenen C API-Änderungen und verschiedenen internen Implementierungsdetails lagen.
Von diesen verbleibenden Unterschieden war der signifikanteste, dass PEP 667 zu diesem Zeitpunkt immer noch einen sofortigen Rückwärtskompatibilitätsbruch für die PyEval_GetLocals() API vorschlug, sobald die PEP angenommen und implementiert wurde.
PEP 667 wurde seitdem geändert, um eine großzügige Frist für die Veralterung der PyEval_GetLocals() API vorzuschlagen, die sie parallel zu den verbesserten Semantiken der neuen PyEval_GetFrameLocals() API unterstützt.
Alle verbleibenden Bedenken hinsichtlich des C API-Designs beziehen sich auf neue Informations-APIs, die zu einem späteren Zeitpunkt hinzugefügt werden können, wenn sie als notwendig erachtet werden, und alle potenziellen Bedenken hinsichtlich der genauen Leistungseigenschaften der Frame-Locals-View-Implementierung werden durch die Verfügbarkeit einer praktikablen Referenzimplementierung aufgewogen.
Entsprechend wurde diese PEP zugunsten der Weiterverfolgung von PEP 667 zurückgezogen.
Hinweis: Bei der Implementierung von PEP 667 wurde deutlich, dass die Begründung und die Auswirkungen davon, dass locals() aktualisiert wird, um unabhängige Schnappschüsse in optimierten Scopes zurückgibt, in keiner der beiden PEPs vollständig klar war. Die Abschnitte Motivation und Begründung in dieser PEP wurden entsprechend aktualisiert (da diese Aspekte für die angenommene PEP 667 gleichermaßen zutreffen).
Zusammenfassung
Die Semantik des locals() Builtins war historisch unter-spezifiziert und daher implementierungsabhängig.
Diese PEP schlägt vor, das Verhalten der CPython 3.10 Referenzimplementierung für die meisten Ausführungsszenarien formell zu standardisieren, mit einigen Anpassungen des Verhaltens im Funktions-Scope, um es vorhersagbarer und unabhängiger von der An- oder Abwesenheit von Tracing-Funktionen zu machen.
Darüber hinaus wird vorgeschlagen, die folgenden Funktionen in die stabile Python C API/ABI aufzunehmen
typedef enum {
PyLocals_UNDEFINED = -1,
PyLocals_DIRECT_REFERENCE = 0,
PyLocals_SHALLOW_COPY = 1,
_PyLocals_ENSURE_32BIT_ENUM = 2147483647
} PyLocals_Kind;
PyLocals_Kind PyLocals_GetKind();
PyObject * PyLocals_Get();
PyObject * PyLocals_GetCopy();
Es wird auch die Hinzufügung mehrerer unterstützender Funktionen und Typdefinitionen zur CPython C API vorgeschlagen.
Motivation
Obwohl die genaue Semantik des locals() Builtins nominell undefiniert ist, hängen in der Praxis viele Python-Programme davon ab, dass sie sich genau so verhält, wie sie sich in CPython verhält (zumindest wenn keine Tracing-Funktionen installiert sind).
Andere Implementierungen wie PyPy replizieren derzeit dieses Verhalten, bis hin zur Replikation von Fehlern bei der Mutation lokaler Variablen, die auftreten können, wenn ein Trace-Hook installiert ist [1].
Während diese PEP das aktuelle CPython-Verhalten ohne installierte Trace-Hooks als weitgehend akzeptabel betrachtet, hält sie das aktuelle Verhalten bei installierten Trace-Hooks für problematisch, da es Fehler wie [1] verursacht, ohne die gewünschte Funktionalität, Debugger wie pdb zum Ändern lokaler Variablen zu ermöglichen, zuverlässig zu ermöglichen [3].
Die Überprüfung der ursprünglichen PEP und der Entwurfsimplementierung identifizierte dann eine Gelegenheit zur Vereinfachung sowohl der Dokumentation als auch der Implementierung des Funktions-Level-Verhaltens von locals(), indem dieses aktualisiert wurde, um bei jedem Aufruf einen unabhängigen Schnappschuss der Funktions-Locals und Closure-Variablen zurückzugeben, anstatt die semi-dynamische, intermittierend aktualisierte geteilte Kopie, die es historisch in CPython zurückgab, beizubehalten.
Insbesondere eliminiert der Vorschlag in dieser PEP das historische Verhalten, bei dem das Hinzufügen einer neuen lokalen Variable das Verhalten von Code, der mit exec() in Funktions-Scopes ausgeführt wird, ändern kann, selbst wenn dieser Code läuft, bevor die lokale Variable definiert ist.
Zum Beispiel:
def f():
exec("x = 1")
print(locals().get("x"))
f()
gibt 1 aus, aber
def f():
exec("x = 1")
print(locals().get("x"))
x = 0
f()
gibt None aus (der Standardwert aus dem .get() Aufruf).
Mit dieser PEP würden beide Beispiele None ausgeben, da der Aufruf von exec() und der nachfolgende Aufruf von locals() unabhängige Dictionary-Schnappschüsse der lokalen Variablen verwenden würden, anstatt dasselbe geteilte Dictionary zu verwenden, das auf dem Frame-Objekt gecacht wird.
Vorschlag
Die erwartete Semantik des locals() Builtins ändert sich basierend auf dem aktuellen Ausführungskontext. Zu diesem Zweck sind die definierten Ausführungskontexte:
- Modul-Scope: Top-Level-Modulcode sowie jeder andere Code, der mit
exec()odereval()mit einem einzigen Namespace ausgeführt wird - Klassen-Scope: Code im Körper einer
classAnweisung sowie jeder andere Code, der mitexec()odereval()mit getrennten lokalen und globalen Namespaces ausgeführt wird - Funktions-Scope: Code im Körper einer
defoderasync defAnweisung oder jede andere Konstruktion, die einen optimierten Codeblock in CPython erstellt (z. B. Comprehensions, Lambda-Funktionen)
Diese PEP schlägt vor, den Großteil des aktuellen Verhaltens der CPython-Referenzimplementierung zu einer Sprachanweisung zu erheben, außer dass jeder Aufruf von locals() im Funktions-Scope ein neues Dictionary-Objekt erstellt, anstatt eine gemeinsame Dict-Instanz im Frame-Objekt zu cachen, die jede aufgerufene Instanz aktualisiert und zurückgibt.
Diese PEP schlägt auch vor, das Konzept eines separaten "Tracing"-Modus aus der CPython-Referenzimplementierung weitgehend zu eliminieren. In Releases bis einschließlich Python 3.10 verhält sich der CPython-Interpreter anders, wenn ein Trace-Hook in einem oder mehreren Threads über einen implementierungsabhängigen Mechanismus wie sys.settrace ([4]) im sys Modul von CPython oder PyEval_SetTrace ([5]) in der C API von CPython registriert wurde. Wenn diese PEP angenommen wird, besteht der einzige verbleibende Verhaltensunterschied bei installiertem Trace-Hook darin, dass einige Optimierungen in der Interpreter-Eval-Schleife deaktiviert werden, wenn die Tracing-Logik nach jedem Opcode ausgeführt werden muss.
Diese PEP schlägt Änderungen am CPython-Verhalten im Funktions-Scope vor, die die locals() Builtin-Semantiken, wenn ein Trace-Hook registriert ist, identisch mit denen machen, die verwendet werden, wenn kein Trace-Hook registriert ist, während gleichzeitig die zugehörige Frame-API-Semantik klarer und für interaktive Debugger einfacher zu verwenden ist.
Die vorgeschlagene Eliminierung des Tracing-Modus wirkt sich auf die Semantik von Frame-Objekt-Referenzen aus, die über andere Mittel erhalten werden, z. B. über einen Traceback oder über die sys._getframe() API, da die für die Trace-Hook-Unterstützung erforderliche Write-Through-Semantik immer durch das f_locals Attribut auf Frame-Objekten bereitgestellt wird und nicht von Laufzeitzuständen abhängt.
Neue locals() Dokumentation
Das Herzstück dieses Vorschlags ist die Überarbeitung der Dokumentation für den locals() Builtin, die wie folgt lauten soll:
Gibt ein Mapping-Objekt zurück, das die aktuelle lokale Symboltabelle darstellt, mit Variablennamen als Schlüsseln und ihren aktuell gebundenen Referenzen als Werten.Im Modul-Scope sowie bei der Verwendung von
exec()odereval()mit einem einzigen Namespace gibt diese Funktion denselben Namespace wieglobals()zurück.Im Klassen-Scope gibt sie den Namespace zurück, der an den Metaklassenkonstruktor übergeben wird.
Bei der Verwendung von
exec()odereval()mit getrennten lokalen und globalen Namespaces gibt sie den lokalen Namespace zurück, der an den Funktionsaufruf übergeben wurde.In allen obigen Fällen gibt jeder Aufruf von
locals()in einem gegebenen Ausführungsframe dasselbe Mapping-Objekt zurück. Änderungen, die über das vonlocals()zurückgegebene Mapping vorgenommen werden, werden als gebundene, neu gebundene oder gelöschte lokale Variablen sichtbar, und das Binden, Neubinden oder Löschen lokaler Variablen wirkt sich unmittelbar auf den Inhalt des zurückgegebenen Mapping-Objekts aus.Im Funktions-Scope (einschließlich Generatoren und Coroutinen) gibt jeder Aufruf von
locals()stattdessen ein neues Dictionary zurück, das die aktuellen Bindungen der lokalen Variablen der Funktion und aller nonlocalen Cell-Referenzen enthält. In diesem Fall werden Namensbindungsänderungen, die über das zurückgegebene Dict vorgenommen werden, *nicht* in die entsprechenden lokalen Variablen oder nonlocalen Cell-Referenzen zurückgeschrieben, und das Binden, Neubinden oder Löschen lokaler Variablen und nonlocaler Cell-Referenzen hat *keine* Auswirkung auf den Inhalt zuvor zurückgegebener Dictionaries.
Es gäbe auch eine versionchanged Notiz für die Veröffentlichung, die diese Änderung vornimmt
In früheren Versionen war die Semantik der Mutation des vonlocals()zurückgegebenen Mapping-Objekts formal undefiniert. In CPython spezifisch konnte das im Funktions-Scope zurückgegebene Mapping durch andere Operationen implizit aktualisiert werden, z. B. durch erneutes Aufrufen vonlocals(), oder der Interpreter rief implizit eine Python-Level-Trace-Funktion auf. Das Erhalten des Legacy-CPython-Verhaltens erfordert nun explizite Aufrufe, um das ursprünglich zurückgegebene Dictionary mit den Ergebnissen nachfolgender Aufrufe vonlocals()zu aktualisieren.
Zur Referenz, die aktuelle Dokumentation dieses Builtins liest sich wie folgt:
Aktualisiert und gibt ein Dictionary zurück, das die aktuelle lokale Symboltabelle darstellt. Freie Variablen werden von locals() zurückgegeben, wenn es in Funktionsblöcken aufgerufen wird, aber nicht in Klassenblöcken.Hinweis: Der Inhalt dieses Dictionaries sollte nicht geändert werden; Änderungen wirken sich möglicherweise nicht auf die Werte lokaler und freier Variablen aus, die vom Interpreter verwendet werden.
(Mit anderen Worten: Der Status quo ist, dass die Semantik und das Verhalten von locals() formell implementierungsdefiniert sind, während der vorgeschlagene Zustand nach dieser PEP ist, dass das einzige implementierungsdefinierte Verhalten dasjenige sein wird, das damit zusammenhängt, ob die Implementierung die CPython Frame-API emuliert oder nicht, wobei das Verhalten in allen anderen Fällen durch die Sprach- und Bibliotheksreferenzen definiert wird)
Modul-Scope
Im Modul-Scope sowie bei der Verwendung von exec() oder eval() mit einem einzigen Namespace muss locals() dasselbe Objekt wie globals() zurückgeben, welches der tatsächliche Ausführungsnamespace sein muss (verfügbar als inspect.currentframe().f_locals in Implementierungen, die Zugriff auf Frame-Objekte bieten).
Variablenzuweisungen während der nachfolgenden Codeausführung im selben Scope müssen den Inhalt des zurückgegebenen Mappings dynamisch ändern, und Änderungen am zurückgegebenen Mapping müssen die Werte ändern, die an lokale Variablennamen in der Ausführungsumgebung gebunden sind.
Um diese Erwartung als Teil der Sprachanweisung zu erfassen, wird der folgende Absatz zur Dokumentation von locals() hinzugefügt:
Im Modul-Scope sowie bei der Verwendung vonexec()odereval()mit einem einzigen Namespace gibt diese Funktion denselben Namespace wieglobals()zurück.
Dieser Teil des Vorschlags erfordert keine Änderungen an der Referenzimplementierung – es handelt sich um eine Standardisierung des aktuellen Verhaltens.
Klassen-Scope
Im Klassen-Scope sowie bei der Verwendung von exec() oder eval() mit getrennten globalen und lokalen Namespaces muss locals() den angegebenen lokalen Namespace zurückgeben (der im Falle von Klassen durch die Metaklassenmethode __prepare__ bereitgestellt werden kann). Wie beim Modul-Scope muss dies eine direkte Referenz auf den tatsächlichen Ausführungsnamespace sein (verfügbar als inspect.currentframe().f_locals in Implementierungen, die Zugriff auf Frame-Objekte bieten).
Variablenzuweisungen während der nachfolgenden Codeausführung im selben Scope müssen den Inhalt des zurückgegebenen Mappings ändern, und Änderungen am zurückgegebenen Mapping müssen die Werte ändern, die an lokale Variablennamen in der Ausführungsumgebung gebunden sind.
Das von locals() zurückgegebene Mapping wird *nicht* als tatsächlicher Klassen-Namespace verwendet, der der definierten Klasse zugrunde liegt (der Klassenbildungsprozess kopiert den Inhalt in ein neues Dictionary, das nur durch den Zugriff auf die Klassenmechanismen zugänglich ist).
Für verschachtelte Klassen, die innerhalb einer Funktion definiert sind, werden keine nonlocalen Cells, auf die im Klassen-Scope verwiesen wird, in das locals() Mapping aufgenommen.
Um diese Erwartung als Teil der Sprachanweisung zu erfassen, werden die folgenden zwei Absätze zur Dokumentation von locals() hinzugefügt:
Bei der Verwendung vonexec()odereval()mit getrennten lokalen und globalen Namespaces gibt [diese Funktion] den gegebenen lokalen Namespace zurück.Im Klassen-Scope gibt sie den Namespace zurück, der an den Metaklassenkonstruktor übergeben wird.
Dieser Teil des Vorschlags erfordert keine Änderungen an der Referenzimplementierung – es handelt sich um eine Standardisierung des aktuellen Verhaltens.
Funktions-Scope
Im Funktions-Scope sind die Interpreter-Implementierungen hinsichtlich der Optimierung des Zugriffs auf lokale Variablen erheblich frei und sind daher NICHT verpflichtet, eine beliebige Modifikation von lokalen und nonlocalen Variablenbindungen über das von locals() zurückgegebene Mapping zuzulassen.
Historisch wurde diese Nachsicht in der Sprachanweisung mit den Worten "Der Inhalt dieses Dictionaries sollte nicht geändert werden; Änderungen wirken sich möglicherweise nicht auf die Werte lokaler und freier Variablen aus, die vom Interpreter verwendet werden." beschrieben.
Diese PEP schlägt vor, diesen Text dahingehend zu ändern, dass er stattdessen lautet:
Im Funktions-Scope (einschließlich Generatoren und Coroutinen) gibt jeder Aufruf vonlocals()stattdessen ein neues Dictionary zurück, das die aktuellen Bindungen der lokalen Variablen der Funktion und aller nonlocalen Cell-Referenzen enthält. In diesem Fall werden Namensbindungsänderungen, die über das zurückgegebene Dict vorgenommen werden, *nicht* in die entsprechenden lokalen Variablen oder nonlocalen Cell-Referenzen zurückgeschrieben, und das Binden, Neubinden oder Löschen lokaler Variablen und nonlocaler Cell-Referenzen hat *keine* Auswirkung auf den Inhalt zuvor zurückgegebener Dictionaries.
Dieser Teil des Vorschlags erfordert *Änderungen* an der CPython-Referenzimplementierung, da CPython derzeit ein gemeinsames Mapping-Objekt zurückgibt, das durch zusätzliche Aufrufe von locals() implizit aktualisiert werden kann, und die "Write-Back"-Strategie, die derzeit zur Unterstützung von Namespace-Änderungen von Trace-Funktionen verwendet wird, entspricht ihr auch nicht (und verursacht die eigenartigen Verhaltensprobleme, die in der Motivation oben erwähnt werden).
CPython Implementierungsänderungen
Zusammenfassung der vorgeschlagenen implementierungsspezifischen Änderungen
- Änderungen werden vorgenommen, um die aktualisierte Python-Level-Semantik bereitzustellen
- Zwei neue Funktionen werden zur stabilen ABI hinzugefügt, um das aktualisierte Verhalten des Python
locals()Builtins zu replizieren
PyObject * PyLocals_Get();
PyLocals_Kind PyLocals_GetKind();
- Eine neue Funktion wird zur stabilen ABI hinzugefügt, um effizient einen Schnappschuss des lokalen Namespaces im laufenden Frame zu erhalten
PyObject * PyLocals_GetCopy();
- Entsprechende Frame-Accessor-Funktionen für diese neuen öffentlichen APIs werden zur CPython Frame C API hinzugefügt
- Auf optimierten Frames gibt die Python-Level
f_localsAPI dynamisch erzeugte Lese-/Schreib-Proxy-Objekte zurück, die direkt auf den lokalen und Closure-Variablenspeicher des Frames zugreifen. Um die Interoperabilität mit der bestehendenPyEval_GetLocals()API zu gewährleisten, verwenden die Proxy-Objekte weiterhin den C-Level Frame-Locals-Datenspeicher als Cache, der auch die Speicherung beliebiger zusätzlicher Schlüssel ermöglicht. Weitere Details zum erwarteten Verhalten dieser schnellen Locals-Proxy-Objekte werden unten behandelt. - Es wird keine C API-Funktion hinzugefügt, um auf ein veränderbares Mapping für den lokalen Namespace zuzugreifen. Stattdessen wird
PyObject_GetAttrString(frame, "f_locals")verwendet, dieselbe API wie im Python-Code. PyEval_GetLocals()bleibt unterstützt und gibt keine programmatische Warnung aus, wird aber in der Dokumentation zugunsten der neuen APIs, die nicht auf der Rückgabe einer geliehenen Referenz basieren, als veraltet markiert.PyFrame_FastToLocals()undPyFrame_FastToLocalsWithError()bleiben unterstützt und geben keine programmatische Warnung aus, werden aber in der Dokumentation zugunsten der neuen APIs, die keinen direkten Zugriff auf das interne Speicherlayout von Frame-Objekten erfordern, als veraltet markiert.PyFrame_LocalsToFast()löst immerRuntimeError()aus und zeigt damit an, dassPyObject_GetAttrString(frame, "f_locals")verwendet werden sollte, um ein veränderbares Lese-/Schreib-Mapping für die lokalen Variablen zu erhalten.- Die Trace-Hook-Implementierung ruft
PyFrame_FastToLocals()nicht mehr implizit auf. Der Portierungsleitfaden wird empfehlen, für schreibgeschützten Zugriff aufPyFrame_GetLocals()und für Lese-/Schreibzugriff aufPyObject_GetAttrString(frame, "f_locals")zu migrieren.
Bereitstellung der aktualisierten Python-Level-Semantik
Die Implementierung des locals() Builtins wird modifiziert, um eine separate Kopie des lokalen Namespaces für optimierte Frames zurückzugeben, anstatt eine direkte Referenz auf den internen Frame-Wert-Cache, der durch die PyFrame_FastToLocals() C API aktualisiert und von der PyEval_GetLocals() C API zurückgegeben wird.
Lösung der Probleme mit dem Trace-Modusverhalten
Die aktuelle Ursache für die Eigenheiten des CPython-Tracing-Modus (sowohl die Seiteneffekte durch die einfache Installation einer Tracing-Funktion als auch die Tatsache, dass das Zurückschreiben von Werten in Funktions-Locals nur für die spezifische Funktion funktioniert, die getraced wird) ist die Art und Weise, wie die Unterstützung für die Mutation von Locals für Trace-Hooks derzeit implementiert ist: die Funktion PyFrame_LocalsToFast.
Wenn eine Trace-Funktion installiert ist, führt CPython derzeit für Funktions-Frames (solche, bei denen das Code-Objekt "fast locals"-Semantiken verwendet) Folgendes aus:
- Ruft
PyFrame_FastToLocals()auf, um den Frame-Wert-Cache zu aktualisieren - Ruft den Trace-Hook auf (wobei das Tracing des Hooks selbst deaktiviert ist)
- Ruft
PyFrame_LocalsToFast()auf, um alle Änderungen am Frame-Wert-Cache zu erfassen
Dieser Ansatz ist aus mehreren Gründen problematisch
- Selbst wenn die Trace-Funktion den Wert-Cache nicht mutiert, setzt der letzte Schritt alle Cell-Referenzen auf den Zustand zurück, in dem sie sich vor dem Aufruf der Trace-Funktion befanden (dies ist die Hauptursache für den Bug-Report in [1])
- Wenn die Trace-Funktion den Wert-Cache mutiert, dann aber etwas tut, das dazu führt, dass der Wert-Cache aus dem Frame aktualisiert wird, gehen diese Änderungen verloren (dies ist ein Aspekt des Bug-Reports in [3])
- Wenn die Trace-Funktion versucht, die lokalen Variablen eines anderen Frames als des getraceten zu mutieren (z. B.
frame.f_back.f_locals), gehen diese Änderungen mit ziemlicher Sicherheit verloren (dies ist ein weiterer Aspekt des Bug-Reports in [3]) - Wenn eine Referenz auf den Frame-Wert-Cache (z. B. über
locals()abgerufen) an eine andere Funktion übergeben wird und *diese* Funktion den Wert-Cache mutiert, dann *können* diese Änderungen in den Ausführungsframe zurückgeschrieben werden, *wenn* ein Trace-Hook installiert ist.
Die vorgeschlagene Lösung für dieses Problem besteht darin, die Tatsache auszunutzen, dass Funktionen typischerweise auf ihren *eigenen* Namespace über den sprachdefinierten locals() Builtin zugreifen, während Trace-Funktionen notwendigerweise die implementierungsabhängige frame.f_locals Schnittstelle verwenden, da ein Frame-Referenz das ist, was an Hook-Implementierungen übergeben wird.
Anstatt einer direkten Referenz auf den internen Frame-Wert-Cache, der historisch vom locals() Builtin zurückgegeben wurde, wird die Python-Level frame.f_locals aktualisiert, um Instanzen eines dedizierten schnellen Locals-Proxy-Typs zurückzugeben, der Werte direkt in und aus dem schnellen Locals-Array auf dem zugrunde liegenden Frame schreibt und liest. Jeder Zugriff auf das Attribut erzeugt eine neue Instanz des Proxys (daher ist die Erstellung von Proxy-Instanzen absichtlich eine kostengünstige Operation).
Obwohl der neue Proxy-Typ der bevorzugte Weg zum Zugriff auf lokale Variablen auf optimierten Frames wird, bleibt der interne Wert-Cache auf dem Frame erhalten für zwei Hauptzwecke:
- Aufrechterhaltung der Abwärtskompatibilität und Interoperabilität mit der
PyEval_GetLocals()C API - Bereitstellung von Speicherplatz für zusätzliche Schlüssel, die keine Slots im schnellen Locals-Array haben (z. B. die Schlüssel
__return__und__exception__, die vonpdbbeim Tracing der Codeausführung zu Debugging-Zwecken gesetzt werden)
Mit den Änderungen in dieser PEP ist dieser interne Frame-Wert-Cache nicht mehr direkt aus Python-Code zugänglich (während er historisch sowohl vom locals() Builtin zurückgegeben wurde als auch als frame.f_locals Attribut verfügbar war). Stattdessen ist der Wert-Cache nur über die PyEval_GetLocals() C API und durch direkten Zugriff auf den internen Speicher eines Frame-Objekts zugänglich.
Schnelle Locals-Proxy-Objekte und der interne Frame-Wert-Cache, der von PyEval_GetLocals() zurückgegeben wird, bieten die folgenden Verhaltensgarantien:
- Änderungen, die über einen schnellen Locals-Proxy vorgenommen werden, sind für den Frame selbst, für andere schnelle Locals-Proxy-Objekte für denselben Frame und im internen Wert-Cache, der auf dem Frame gespeichert ist, sofort sichtbar (dies ist der letzte Punkt, der die Interoperabilität von
PyEval_GetLocals()gewährleistet) - Änderungen, die direkt am internen Frame-Wert-Cache vorgenommen werden, sind niemals für den Frame selbst sichtbar und sind nur dann zuverlässig über schnelle Locals-Proxys für denselben Frame sichtbar, wenn sich die Änderung auf zusätzliche Variablen bezieht, die keine Slots im schnellen Locals-Array des Frames haben.
- Änderungen, die durch die Ausführung von Code im Frame vorgenommen werden, sind für alle schnellen Locals-Proxy-Objekte für diesen Frame (sowohl bestehende als auch neu erstellte Proxys) sofort sichtbar. Die Sichtbarkeit im internen Frame-Wert-Cache, der von
PyEval_GetLocals()zurückgegeben wird, unterliegt den Cache-Update-Richtlinien, die im nächsten Abschnitt erläutert werden.
Als Ergebnis dieser Punkte müssen nur Code, der PyEval_GetLocals(), PyLocals_Get() oder PyLocals_GetCopy() verwendet, berücksichtigen, dass der Frame-Wert-Cache möglicherweise veraltet ist. Code, der die neue Frame-Fast-Locals-Proxy-API (sowohl aus Python als auch aus C) verwendet, sieht immer den Live-Status des Frames.
Details zur Implementierung des schnellen Locals-Proxys
Jede Instanz eines schnellen Locals-Proxys hat ein einzelnes internes Attribut, das nicht als Teil der Python-Runtime-API exponiert wird
- frame: der zugrunde liegende optimierte Frame, auf den der Proxy Zugriff bietet
Zusätzlich verwenden und aktualisieren Proxy-Instanzen die folgenden Attribute, die auf dem zugrunde liegenden Frame oder Code-Objekt gespeichert sind:
- _name_to_offset_mapping: ein verstecktes Mapping von Variablennamen zu schnellen Local-Speicher-Offsets. Dieses Mapping wird beim ersten Lesen oder Schreiben von Frame-Daten über einen schnellen Locals-Proxy lazy initialisiert, anstatt sofort beim Erstellen des ersten schnellen Locals-Proxys aufgefüllt zu werden. Da das Mapping für alle Frames, die denselben Code-Objekt ausführen, identisch ist, wird eine einzelne Kopie auf dem Code-Objekt gespeichert, anstatt dass jedes Frame-Objekt sein eigenes Mapping aufbaut.
- locals: der interne Frame-Wert-Cache, der von der
PyEval_GetLocals()C API zurückgegeben und von derPyFrame_FastToLocals()C API aktualisiert wird. Dies ist das Mapping, das derlocals()Builtin in Python 3.10 und früheren Versionen zurückgibt.
__getitem__ Operationen auf dem Proxy füllen das _name_to_offset_mapping auf dem Code-Objekt auf (falls es noch nicht gefüllt ist) und geben dann entweder den relevanten Wert zurück (wenn der Schlüssel sowohl im _name_to_offset_mapping Mapping als auch im internen Frame-Wert-Cache gefunden wird) oder lösen KeyError aus. Variablen, die auf dem Frame definiert, aber derzeit nicht gebunden sind, lösen ebenfalls KeyError aus (genau wie sie aus dem Ergebnis von locals() weggelassen werden).
Da der Frame-Speicher immer direkt zugänglich ist, erkennt der Proxy automatisch Namensbindungs- und Unbindungsoperationen, die während der Ausführung der Funktion stattfinden. Der interne Wert-Cache wird implizit aktualisiert, wenn einzelne Variablen aus dem Frame-Status gelesen werden (einschließlich Containment-Prüfungen, die prüfen müssen, ob der Name derzeit gebunden oder ungebunden ist).
Ähnlich werden __setitem__ und __delitem__ Operationen auf dem Proxy die entsprechende schnelle lokale oder Cell-Referenz auf dem zugrunde liegenden Frame direkt beeinflussen, was sicherstellt, dass Änderungen für den laufenden Python-Code sofort sichtbar sind, anstatt zu einem späteren Zeitpunkt zurück in den Laufzeitspeicher geschrieben werden zu müssen. Solche Änderungen werden auch sofort in den internen Frame-Wert-Cache geschrieben, um sie für Benutzer der PyEval_GetLocals() C API sichtbar zu machen.
Schlüssel, die nicht als lokale oder Closure-Variablen auf dem zugrunde liegenden Frame definiert sind, werden weiterhin in den internen Wert-Cache auf optimierten Frames geschrieben. Dies ermöglicht es Dienstprogrammen wie pdb (das __return__ und __exception__ Werte in das f_locals Mapping des Frames schreibt), weiterhin wie gewohnt zu funktionieren. Diese zusätzlichen Schlüssel, die keiner lokalen oder Closure-Variable auf dem Frame entsprechen, werden von zukünftigen Cache-Synchronisierungsoperationen nicht verändert. Die Verwendung des Frame-Wert-Caches zur Speicherung dieser zusätzlichen Schlüssel (anstelle der Definition eines neuen Mappings, das nur die zusätzlichen Schlüssel enthält) bietet volle Interoperabilität mit der bestehenden PyEval_GetLocals() API (da Benutzer einer der beiden APIs die zusätzlichen Schlüssel sehen, die von Benutzern der anderen API hinzugefügt wurden, anstatt dass Benutzer der neuen schnellen Locals-Proxy-API nur Schlüssel sehen, die über diese API hinzugefügt wurden).
Ein zusätzlicher Vorteil der ausschließlichen Speicherung des Variable-Value-Caches im Frame (anstatt einer Instanz des Proxy-Typs) ist, dass dadurch ein Referenzzyklus vom Frame zu sich selbst vermieden wird. Der Frame wird also nur dann am Leben gehalten, wenn ein anderes Objekt eine Referenz auf eine Proxy-Instanz behält.
Hinweis: Das Aufrufen der Methode proxy.clear() hat einen ähnlich weitreichenden Effekt wie das Aufrufen von PyFrame_LocalsToFast() bei einem leeren Variable-Value-Cache des Frames in früheren Versionen. Nicht nur die lokalen Variablen des Frames werden gelöscht, sondern auch alle Cell-Variablen, auf die vom Frame aus zugegriffen werden kann (unabhängig davon, ob diese Cells vom Frame selbst oder von einem äußeren Frame besessen werden). Dies kann die __class__-Zelle einer Klasse löschen, wenn es auf dem Frame einer Methode aufgerufen wird, die den Null-Argument-Konstrukt super() verwendet (oder anderweitig auf __class__ verweist). Dies geht über den Umfang des Aufrufs von frame.clear() hinaus, da dieser lediglich die Referenzen des Frames auf Cell-Variablen löscht, aber nicht die Cells selbst löscht. Diese PEP könnte eine potenzielle Gelegenheit sein, den Umfang von Versuchen, die Frame-Variablen direkt zu löschen, einzugrenzen, indem die Cells, die zu äußeren Frames gehören, ignoriert werden und nur lokale Variablen und Cells gelöscht werden, die direkt zum Frame gehören, der dem Proxy zugrunde liegt (dieses Problem betrifft auch PEP 667, da die Frage die Handhabung von Cell-Variablen betrifft und völlig unabhängig vom internen Frame-Value-Cache ist).
Änderungen an der stabilen C API/ABI
Im Gegensatz zu Python-Code können Erweiterungsmodulfunktionen, die die Python C API aufrufen, aus jeder Art von Python-Scope aufgerufen werden. Das bedeutet, dass aus dem Kontext nicht ersichtlich ist, ob locals() einen Schnappschuss zurückgibt oder nicht, da dies vom Scope des aufrufenden Python-Codes abhängt und nicht vom C-Code selbst.
Dies bedeutet, dass es wünschenswert ist, C-APIs anzubieten, die ein vorhersagbares, Scope-unabhängiges Verhalten liefern. Es ist jedoch auch wünschenswert, dass C-Code das Verhalten von Python-Code im selben Scope exakt nachahmen kann.
Um das Verhalten von Python-Code nachahmen zu können, würde die stabile C ABI die folgenden neuen Funktionen erhalten:
PyObject * PyLocals_Get();
PyLocals_Kind PyLocals_GetKind();
PyLocals_Get() ist direkt äquivalent zum eingebauten Python-Befehl locals(). Es gibt eine neue Referenz auf das lokale Namespace-Mapping für den aktiven Python-Frame im Modul- und Klassen-Scope zurück und bei Verwendung von exec() oder eval(). Es gibt eine flache Kopie des aktiven Namespaces im Funktions-/Coroutine-/Generator-Scope zurück.
PyLocals_GetKind() gibt einen Wert aus der neu definierten Enumeration PyLocals_Kind zurück, wobei die folgenden Optionen verfügbar sind:
PyLocals_DIRECT_REFERENCE:PyLocals_Get()gibt eine direkte Referenz auf das lokale Namespace des laufenden Frames zurück.PyLocals_SHALLOW_COPY:PyLocals_Get()gibt eine flache Kopie des lokalen Namespace des laufenden Frames zurück.PyLocals_UNDEFINED: Es ist ein Fehler aufgetreten (z. B. kein aktiver Python-Thread-Status). Eine Python-Ausnahme wird gesetzt, wenn dieser Wert zurückgegeben wird.
Da die Enumeration in der stabilen ABI verwendet wird, wird ein zusätzlicher 31-Bit-Wert gesetzt, um sicherzustellen, dass es sicher ist, beliebige vorzeichenbehaftete 32-Bit-Ganzzahlen in PyLocals_Kind-Werte umzuwandeln.
Diese Abfrage-API ermöglicht es Erweiterungsmodulcode, die potenziellen Auswirkungen der Änderung des von PyLocals_Get() zurückgegebenen Mappings zu ermitteln, ohne auf die Details des laufenden Frame-Objekts zugreifen zu müssen. Python-Code erhält visuell eine äquivalente Information durch lexikalische Geltungsbereiche (wie in der Dokumentation des neuen eingebauten Befehls locals() behandelt).
Um zu ermöglichen, dass Erweiterungsmodulcode konsistent unabhängig vom aktiven Python-Scope agiert, würde die stabile C-ABI die folgende neue Funktion erhalten:
PyObject * PyLocals_GetCopy();
PyLocals_GetCopy() gibt eine neue Dict-Instanz zurück, die aus dem aktuellen Locals-Namespace gefüllt ist. Dies entspricht grob dict(locals()) in Python-Code, vermeidet jedoch die doppelte Kopie, wenn locals() bereits eine flache Kopie zurückgibt. Ähnlich wie beim folgenden Code, aber ohne anzunehmen, dass es jemals nur zwei Arten von Locals-Ergebnissen geben wird:
locals = PyLocals_Get();
if (PyLocals_GetKind() == PyLocals_DIRECT_REFERENCE) {
locals = PyDict_Copy(locals);
}
Die bestehende PyEval_GetLocals() API behält ihr bestehendes Verhalten in CPython (mutable Locals in Klasse und Modul-Scope, ansonsten gemeinsam genutzter dynamischer Schnappschuss) bei. Ihre Dokumentation wird jedoch aktualisiert, um darauf hinzuweisen, dass sich die Bedingungen geändert haben, unter denen der gemeinsam genutzte dynamische Schnappschuss aktualisiert wird.
Die Dokumentation von PyEval_GetLocals() wird auch aktualisiert, um die Verwendung dieser API durch eine der neuen APIs zu ersetzen, die für den jeweiligen Anwendungsfall am besten geeignet ist, zu empfehlen.
- Verwenden Sie
PyLocals_Get()(optional kombiniert mitPyDictProxy_New()) für schreibgeschützten Zugriff auf den aktuellen Locals-Namespace. Diese Nutzungsform muss berücksichtigen, dass die Kopie in optimierten Frames veraltet sein kann. - Verwenden Sie
PyLocals_GetCopy()für ein reguläres mutable Dict, das eine Kopie des aktuellen Locals-Namespaces enthält, aber keine laufende Verbindung zum aktiven Frame hat. - Verwenden Sie
PyLocals_Get(), um die Semantik des Python-Level-Befehlslocals()exakt nachzubilden. - Fragen Sie
PyLocals_GetKind()explizit ab, um eine benutzerdefinierte Behandlung (z. B. Auslösen einer aussagekräftigen Ausnahme) für Scopes zu implementieren, in denenPyLocals_Get()eine flache Kopie zurückgibt, anstatt Lese-/Schreibzugriff auf den Locals-Namespace zu gewähren. - Verwenden Sie implementierungsspezifische APIs (z. B.
PyObject_GetAttrString(frame, "f_locals")), wenn Lese-/Schreibzugriff auf den Frame erforderlich ist undPyLocals_GetKind()etwas anderes alsPyLocals_DIRECT_REFERENCEzurückgibt.
Änderungen an der öffentlichen CPython C API
Die bestehende PyEval_GetLocals() API gibt eine geborgte Referenz zurück, was bedeutet, dass sie nicht aktualisiert werden kann, um die neuen flachen Kopien im Funktions-Scope zurückzugeben. Stattdessen gibt sie weiterhin eine geborgte Referenz auf einen internen dynamischen Schnappschuss zurück, der im Frame-Objekt gespeichert ist. Dieses gemeinsam genutzte Mapping verhält sich ähnlich wie das bestehende gemeinsam genutzte Mapping in Python 3.10 und früher, aber die genauen Bedingungen, unter denen es aktualisiert wird, werden unterschiedlich sein. Konkret wird es nur unter den folgenden Umständen aktualisiert:
- jeder Aufruf von
PyEval_GetLocals(),PyLocals_Get(),PyLocals_GetCopy()oder der Python-eingebaute Befehllocals(), während der Frame läuft - jeder Aufruf von
PyFrame_GetLocals(),PyFrame_GetLocalsCopy(),_PyFrame_BorrowLocals(),PyFrame_FastToLocals()oderPyFrame_FastToLocalsWithError()für den Frame - jede Operation auf einem schnellen Locals-Proxy-Objekt, die das gemeinsam genutzte Mapping als Teil seiner Implementierung aktualisiert. In der ursprünglichen Referenzimplementierung sind dies Operationen, die von Natur aus
O(n)sind (len(flp), Mapping-Vergleich,flp.copy()und String-Darstellung) sowie solche, die die Cache-Einträge für einzelne Schlüssel aktualisieren.
Die Anforderung eines schnellen Locals-Proxy-Objekts aktualisiert den dynamischen Schnappschuss nicht implizit, und die CPython-Trace-Hook-Verwaltung aktualisiert ihn ebenfalls nicht mehr implizit.
(Hinweis: Obwohl PyEval_GetLocals() Teil der stabilen C API/ABI ist, sind die Details, wann das zurückgegebene Namespace aktualisiert wird, immer noch ein Implementierungsdetail des Interpreters).
Die Ergänzungen zur öffentlichen CPython C API sind die Frame-Level-Erweiterungen, die zur Unterstützung der Updates der stabilen C API/ABI erforderlich sind.
PyLocals_Kind PyFrame_GetLocalsKind(frame);
PyObject * PyFrame_GetLocals(frame);
PyObject * PyFrame_GetLocalsCopy(frame);
PyObject * _PyFrame_BorrowLocals(frame);
PyFrame_GetLocalsKind(frame) ist die zugrunde liegende API für PyLocals_GetKind().
PyFrame_GetLocals(frame) ist die zugrunde liegende API für PyLocals_Get().
PyFrame_GetLocalsCopy(frame) ist die zugrunde liegende API für PyLocals_GetCopy().
_PyFrame_BorrowLocals(frame) ist die zugrunde liegende API für PyEval_GetLocals(). Der Unterstrich-Präfix soll die Verwendung davon abraten und anzeigen, dass Code, der ihn verwendet, wahrscheinlich nicht portierbar über Implementierungen hinweg ist. Er ist jedoch dokumentiert und für den Linker sichtbar, um zu vermeiden, dass aus der Implementierung von PyEval_GetLocals() auf die Interna der Frame-Struktur zugegriffen werden muss.
Die Funktion PyFrame_LocalsToFast() wird geändert, um immer RuntimeError auszulösen und zu erklären, dass sie keine unterstützte Operation mehr ist. Betroffener Code sollte aktualisiert werden, um PyObject_GetAttrString(frame, "f_locals") zum Abrufen eines Lese-/Schreib-Proxys zu verwenden.
Zusätzlich zu den oben genannten dokumentierten Schnittstellen gibt die Entwurfsreferenzimplementierung auch die folgenden undokumentierten Schnittstellen frei:
PyTypeObject _PyFastLocalsProxy_Type;
#define _PyFastLocalsProxy_CheckExact(self) Py_IS_TYPE(op, &_PyFastLocalsProxy_Type)
Dieser Typ ist es, den die Referenzimplementierung tatsächlich von PyObject_GetAttrString(frame, "f_locals") für optimierte Frames zurückgibt (d. h. wenn PyFrame_GetLocalsKind() PyLocals_SHALLOW_COPY zurückgibt).
Reduzierung des Laufzeit-Overheads von Trace-Hooks
Wie in [9] erwähnt, ist der implizite Aufruf von PyFrame_FastToLocals() in der Unterstützung für Python-Trace-Hooks nicht kostenlos und könnte unnötig werden, wenn der Frame-Proxy-Lesewert direkt aus dem Frame liest, anstatt ihn aus dem Mapping zu erhalten.
Da der neue Frame-Locals-Proxy-Typ keine separaten Datenaktualisierungsschritte erfordert, nimmt diese PEP Victor Stinners Vorschlag auf, den impliziten Aufruf von PyFrame_FastToLocalsWithError() vor dem Aufruf von Trace-Hooks, die in Python implementiert sind, nicht mehr durchzuführen.
Code, der die neuen schnellen Locals-Proxy-Objekte verwendet, wird den dynamischen Locals-Schnappschuss implizit aktualisieren, wenn auf Methoden zugegriffen wird, die ihn benötigen, während Code, der die PyEval_GetLocals() API verwendet, ihn implizit aktualisieren wird, wenn er diesen Aufruf tätigt.
Die PEP verwirft notwendigerweise auch den impliziten Aufruf von PyFrame_LocalsToFast() beim Zurückkehren aus einem Trace-Hook, da diese API jetzt immer eine Ausnahme auslöst.
Begründung und Design-Diskussion
Änderung von locals(), um unabhängige Schnappschüsse im Funktions-Scope zurückzugeben
Der eingebaute Befehl locals() ist ein erforderlicher Teil der Sprache, und in der Referenzimplementierung gab er historisch ein mutable Mapping mit den folgenden Eigenschaften zurück:
- jeder Aufruf von
locals()gibt dasselbe Mapping-Objekt zurück - für Namespaces, in denen
locals()eine Referenz auf etwas anderes als den tatsächlichen lokalen Ausführungs-Namespace zurückgibt, aktualisiert jeder Aufruf vonlocals()das Mapping-Objekt mit dem aktuellen Zustand der lokalen Variablen und aller referenzierten nonlocal Cells - Änderungen am zurückgegebenen Mapping werden *normalerweise* nicht zurück in die lokalen Variablenbindungen oder die nonlocal Cell-Referenzen geschrieben, aber Schreibvorgänge können ausgelöst werden, indem eines der folgenden durchgeführt wird:
- Installation eines Trace-Hooks auf Python-Ebene (Schreibvorgänge erfolgen dann, wenn der Trace-Hook aufgerufen wird)
- Ausführen eines Wildcard-Imports auf Funktionsebene (erfordert Bytecode-Injektion in Py3)
- Ausführen einer
exec-Anweisung im Scope der Funktion (nur Py2, seitexecin Python 3 eine gewöhnliche eingebaute Funktion wurde)
Ursprünglich schlug diese PEP vor, die ersten beiden dieser Eigenschaften beizubehalten und die dritte zu ändern, um die offensichtlichen Verhaltensfehler zu beheben, die sie verursachen kann.
In [7] legte Nathaniel Smith überzeugend dar, dass wir das Verhalten von locals() im Funktions-Scope erheblich weniger verwirrend gestalten können, indem wir nur die zweite Eigenschaft beibehalten und jeder Aufruf von locals() im Funktions-Scope einen unabhängigen Schnappschuss der lokalen Variablen und Closure-Referenzen zurückgibt, anstatt einen implizit gemeinsam genutzten Schnappschuss zu aktualisieren.
Da dieses überarbeitete Design auch die Implementierung merklich einfacher zu verstehen machte, wurde die PEP aktualisiert, um diese Verhaltensänderung vorzuschlagen, anstatt den historischen gemeinsam genutzten Schnappschuss beizubehalten.
Beibehaltung von locals() als Schnappschuss im Funktions-Scope
Wie in [7] diskutiert, wäre es theoretisch möglich, die Semantik des eingebauten Befehls locals() zu ändern, um im Funktions-Scope einen Write-Through-Proxy zurückzugeben, anstatt ihn auf die Rückgabe unabhängiger Schnappschüsse umzustellen.
Diese PEP schlägt dies nicht vor (und wird es auch nicht), da es sich in der Praxis um eine abwärtskompatible Änderung handelt, auch wenn Code, der sich auf das aktuelle Verhalten verlässt, technisch gesehen in einem undefinierten Bereich der Sprachspezifikation operiert.
Betrachten Sie den folgenden Code-Schnipsel:
def example():
x = 1
locals()["x"] = 2
print(x)
Selbst mit einem installierten Trace-Hook wird diese Funktion in der aktuellen Referenzinterpreterimplementierung konsistent 1 ausgeben.
>>> example()
1
>>> import sys
>>> def basic_hook(*args):
... return basic_hook
...
>>> sys.settrace(basic_hook)
>>> example()
1
Ebenso kann locals() an die eingebauten Funktionen exec() und eval() im Funktions-Scope (explizit oder implizit) übergeben werden, ohne unerwartete Neubindungen von lokalen Variablen oder Closure-Referenzen zu riskieren.
Die Auslösung des Referenzinterpreters zur fehlerhaften Veränderung des Zustands lokaler Variablen erfordert eine komplexere Einrichtung, bei der eine verschachtelte Funktion eine Variable abschließt, die in der äußeren Funktion neu gebunden wird, und aufgrund der Verwendung von Threads, Generatoren oder Coroutinen ist es möglich, dass eine Trace-Funktion für die verschachtelte Funktion ausgeführt wird, bevor die Neubindungsoperation in der äußeren Funktion stattfindet, aber nach Abschluss der Neubindungsoperation abgeschlossen ist (in diesem Fall wird die Neubindung rückgängig gemacht, was der Fehler ist, der in [1] gemeldet wurde).
Zusätzlich zur Beibehaltung der De-facto-Semantik, die seit der Einführung von verschachtelten Scopes in PEP 227 in Python 2.1 besteht, ist der weitere Vorteil der Beschränkung der Write-Through-Proxy-Unterstützung auf die implementierungsdefinierte Frame-Objekt-API, dass nur Interpreterimplementierungen, die die vollständige Frame-API emulieren, die Write-Through-Fähigkeit überhaupt anbieten müssen, und dass JIT-kompilierte Implementierungen sie nur aktivieren müssen, wenn eine Frame-Introspektions-API aufgerufen wird oder ein Trace-Hook installiert ist, nicht jedes Mal, wenn auf locals() im Funktions-Scope zugegriffen wird.
Die Rückgabe von Schnappschüssen von locals() im Funktions-Scope bedeutet auch, dass die statische Analyse für Code auf Funktionsebene zuverlässiger wird, da nur der Zugriff auf die Frame-Mechanik eine Neubindung von lokalen und nonlocal Variablenreferenzen ermöglicht, die der statischen Analyse verborgen bleibt.
Was passiert mit den Standardargumenten für eval() und exec()?
Diese sind formal definiert als Vererbung von globals() und locals() vom aufrufenden Scope per Standard.
Es besteht keine Notwendigkeit für die PEP, diese Standardwerte zu ändern, daher tut sie dies nicht, und exec() und eval() beginnen in einer flachen Kopie des lokalen Namespaces zu laufen, wenn dies das ist, was locals() zurückgibt.
Dieses Verhalten hat potenzielle Leistungsauswirkungen, insbesondere für Funktionen mit einer großen Anzahl lokaler Variablen (z. B. wenn diese Funktionen in einer Schleife aufgerufen werden, das einmalige Aufrufen von globals() und locals() vor der Schleife und das anschließende explizite Übergeben des Namespaces an die Funktion liefert dieselbe Semantik und Leistungseigenschaften wie der Status quo, während die Abhängigkeit vom impliziten Standardwert in jeder Iteration eine neue flache Kopie des lokalen Namespaces erstellen würde).
(Hinweis: Der Entwurfs-PR der Referenzimplementierung hat die eingebauten Funktionen locals() und vars(), eval() und exec() aktualisiert, um PyLocals_Get() zu verwenden. Die eingebaute Funktion dir() verwendet weiterhin PyEval_GetLocals(), da sie sie nur zum Erstellen einer Liste aus den Schlüsseln verwendet.)
Zusätzliche Überlegungen zu eval() und exec() in optimierten Scopes
Hinweis: Bei der Implementierung von PEP 667 wurde festgestellt, dass weder diese noch jene PEP die Auswirkungen der Änderungen an locals() auf Code-Ausführungs-APIs wie exec() und eval() klar erläutert haben. Dieser Abschnitt wurde der Begründung dieser PEP hinzugefügt, um die Auswirkungen besser zu beschreiben und die beabsichtigten Vorteile der Änderung zu erklären.
Als exec() in Python 3.0 von einer Anweisung zu einer eingebauten Funktion konvertiert wurde (Teil der Kernsprachenänderungen in PEP 3100), wurde der damit verbundene implizite Aufruf von PyFrame_LocalsToFast() entfernt, sodass es typischerweise so erscheint, als würden Versuche, lokale Variablen mit exec() in optimierten Frames zu schreiben, ignoriert.
>>> def f():
... x = 0
... exec("x = 1")
... print(x)
... print(locals()["x"])
...
>>> f()
0
0
Tatsächlich werden die Schreibvorgänge nicht ignoriert, sie werden nur nicht vom Dictionary-Cache in das optimierte Array lokaler Variablen kopiert. Die Änderungen am Dictionary werden dann überschrieben, wenn der Dictionary-Cache das nächste Mal aus dem Array aktualisiert wird.
>>> def f():
... x = 0
... locals_cache = locals()
... exec("x = 1")
... print(x)
... print(locals_cache["x"])
... print(locals()["x"])
...
>>> f()
0
1
0
Das Verhalten wird noch seltsamer, wenn eine Tracing-Funktion oder ein anderer Code PyFrame_LocalsToFast() aufruft, bevor der Cache das nächste Mal aktualisiert wird. In diesen Fällen wird die Änderung tatsächlich in das optimierte Array lokaler Variablen geschrieben.
>>> from sys import _getframe
>>> from ctypes import pythonapi, py_object, c_int
>>> _locals_to_fast = pythonapi.PyFrame_LocalsToFast
>>> _locals_to_fast.argtypes = [py_object, c_int]
>>> def f():
... _frame = _getframe()
... _f_locals = _frame.f_locals
... x = 0
... exec("x = 1")
... _locals_to_fast(_frame, 0)
... print(x)
... print(locals()["x"])
... print(_f_locals["x"])
...
>>> f()
1
1
1
Diese Situation war in Python 3.10 und früheren Versionen häufiger, da die bloße Installation einer Tracing-Funktion ausreichte, um implizite Aufrufe von PyFrame_LocalsToFast() nach jeder Zeile Python-Code auszulösen. Dies kann jedoch auch in Python 3.11+ vorkommen, abhängig davon, welche Tracing-Funktionen aktiv sind (z. B. interaktive Debugger tun dies absichtlich, damit Änderungen, die an der Debugger-Eingabeaufforderung vorgenommen werden, sichtbar sind, wenn die Code-Ausführung fortgesetzt wird).
Alle oben genannten Anmerkungen zu exec() gelten für jeden Versuch, das Ergebnis von locals() in optimierten Scopes zu ändern, und sind der Hauptgrund dafür, dass die Dokumentation des eingebauten Befehls locals() diesen Vorbehalt enthält:
Hinweis: Der Inhalt dieses Dictionaries sollte nicht geändert werden; Änderungen wirken sich möglicherweise nicht auf die Werte lokaler und freier Variablen aus, die vom Interpreter verwendet werden.
Obwohl die genaue Formulierung in der Bibliotheksreferenz nicht ganz explizit ist, verwenden sowohl exec() als auch eval() seit langem die Ergebnisse des Aufrufs von globals() und locals() im aufrufenden Python-Frame als ihren Standard-Ausführungs-Namespace.
Dies war historisch auch äquivalent zur Verwendung der Attribute frame.f_globals und frame.f_locals des aufrufenden Frames, aber diese PEP ordnet die Standard-Namespace-Argumente für exec() und eval() globals() und locals() im aufrufenden Frame zu, um die Eigenschaft beizubehalten, dass versuchte Schreibvorgänge in den lokalen Namespace in optimierten Scopes standardmäßig ignoriert werden.
Dies stellt ein potenzielles Kompatibilitätsproblem für einige Codes dar, da mit der vorherigen Implementierung, die dasselbe Dict zurückgibt, wenn locals() mehrmals im Funktions-Scope aufgerufen wird, der folgende Code aufgrund des implizit gemeinsam genutzten lokalen Variablen-Namespaces normalerweise funktionierte:
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()
Wenn locals() in einem optimierten Scope dasselbe gemeinsam genutzte Dict für jeden Aufruf zurückgibt, war es möglich, zusätzliche "Fake-Locals" in diesem Dict zu speichern. Obwohl dies keine echten Locals sind, die vom Compiler bekannt sind (sodass sie nicht mit Code wie print(a) ausgegeben werden können), können sie dennoch über locals() aufgerufen und zwischen mehreren exec()-Aufrufen im selben Funktions-Scope geteilt werden. Darüber hinaus werden sie, da sie keine echten Locals sind, nicht implizit aktualisiert oder entfernt, wenn der gemeinsam genutzte Cache aus dem Array der lokalen Variablen aktualisiert wird.
Wenn der Code in exec() versucht, eine vorhandene lokale Variable zu schreiben, wird das Laufzeitverhalten schwieriger vorhersehbar:
def f():
a = None
exec('a = 0') # equivalent to exec('a = 0', globals(), locals())
exec('print(a)') # equivalent to exec('print(a)', globals(), locals())
print(locals()) # {'a': None}
f()
print(a) gibt None aus, da der implizite locals()-Aufruf in exec() das gecachte Dict mit den tatsächlichen Werten des Frames aktualisiert. Das bedeutet, dass im Gegensatz zu den "Fake"-Locals, die durch Zurückschreiben in locals() (auch durch frühere Aufrufe von exec()) erstellt werden, die echten, vom Compiler bekannten Locals nicht einfach von exec() geändert werden können (dies ist möglich, erfordert aber sowohl das Abrufen des frame.f_locals-Attributs, um Schreibvorgänge in den Frame zu ermöglichen, als auch das anschließende Aufrufen von PyFrame_LocalsToFast(), wie oben mit ctypes gezeigt).
Wie im Abschnitt Motivation erwähnt, tritt dieser verwirrende Nebeneffekt selbst dann auf, wenn die lokale Variable erst *nach* den exec()-Aufrufen definiert wird.
>>> def f():
... exec("a = 0")
... exec("print('a' in locals())") # Printing 'a' directly won't work
... print(locals())
... a = None
... print(locals())
...
>>> f()
False
{}
{'a': None}
Da a eine echte lokale Variable ist, die derzeit nicht an einen Wert gebunden ist, wird sie aus dem Dictionary entfernt, das von locals() zurückgegeben wird, wann immer locals() vor der Zeile a = None aufgerufen wird. Diese Entfernung ist beabsichtigt, da sie es ermöglicht, den Inhalt von locals() in optimierten Scopes korrekt zu aktualisieren, wenn del-Anweisungen verwendet werden, um zuvor gebundene lokale Variablen zu löschen.
Wie im ctypes Beispiel erwähnt, kann die obige Verhaltensbeschreibung ungültig werden, wenn die CPython PyFrame_LocalsToFast() API aufgerufen wird, während der Frame noch läuft. In diesem Fall könnten die Änderungen an a für den laufenden Code sichtbar werden, abhängig davon, wann genau diese API aufgerufen wird (und ob der Frame für die Modifikation von Locals durch Zugriff auf das Attribut frame.f_locals vorbereitet wurde).
Wie oben beschrieben, wurden zwei Optionen zur Ersetzung dieses verwirrenden Verhaltens in Betracht gezogen:
- Lokale Variablen sollen Write-Through-Proxy-Instanzen zurückgeben (ähnlich wie
frame.f_locals) - Lokale Variablen sollen tatsächlich unabhängige Schnappschüsse zurückgeben, sodass Versuche, die Werte lokaler Variablen über
exec()zu ändern, konsistent ignoriert werden, ohne die oben genannten Einschränkungen.
Die PEP wählt aus folgenden Gründen die zweite Option:
- Die Rückgabe unabhängiger Schnappschüsse in optimierten Scopes bewahrt die Änderung von
exec()in Python 3.0, die dazu führte, dass Versuche, lokale Variablen überexec()zu ändern, in den meisten Fällen ignoriert wurden. - Die Unterscheidung zwischen "
locals()gibt einen momentanen Schnappschuss der lokalen Variablen in optimierten Scopes und Lese-/Schreibzugriff in anderen Scopes" und "frame.f_localsgewährt Lese-/Schreibzugriff auf die lokalen Variablen in allen Scopes, einschließlich optimierter Scopes" lässt die Absicht eines Code-Abschnitts klarer erkennen, als wenn beide APIs vollständigen Lese-/Schreibzugriff in optimierten Scopes gewähren würden, auch wenn Schreibzugriff nicht benötigt oder gewünscht wurde. - Zusätzlich zur Verbesserung der Klarheit für menschliche Leser ermöglicht die Sicherstellung, dass die Namensneubindung in optimierten Scopes lexikalisch im Code sichtbar bleibt (solange die Frame-Introspektions-APIs nicht aufgerufen werden), Compilern und Interpretern, verwandte Leistungsoptimierungen konsistenter anzuwenden.
- Nur Python-Implementierungen, die die optionalen Frame-Introspektions-APIs unterstützen, müssen die neue Write-Through-Proxy-Unterstützung für optimierte Frames bereitstellen.
Mit den semantischen Änderungen an locals() in dieser PEP wird es viel einfacher, das Verhalten von exec() und eval() zu erklären: In optimierten Scopes werden sie lokale Variablen niemals implizit beeinflussen; in anderen Scopes werden sie lokale Variablen immer implizit beeinflussen. In optimierten Scopes werden alle impliziten Zuweisungen zu den lokalen Variablen verworfen, wenn die Code-Ausführungs-API zurückkehrt, da bei jeder Invokation eine neue Kopie der lokalen Variablen verwendet wird.
Beibehaltung des internen Frame-Wert-Caches
Die Beibehaltung des internen Frame-Value-Caches führt zu einigen sichtbaren Eigenheiten, wenn Frame-Proxy-Instanzen erhalten bleiben und nach Namensbindungs- und -entbindungsoperationen, die auf dem Frame ausgeführt wurden, wiederverwendet werden.
Der Hauptgrund für die Beibehaltung des Frame-Value-Caches ist die Aufrechterhaltung der Abwärtskompatibilität mit der PyEval_GetLocals() API. Diese API gibt eine geborgte Referenz zurück, daher muss sie sich auf persistente Zustände beziehen, die auf dem Frame-Objekt gespeichert sind. Das Speichern eines schnellen Locals-Proxy-Objekts im Frame erzeugt einen problematischen Referenzzyklus. Daher ist die sauberste Option, weiterhin einen Frame-Value-Cache zurückzugeben, so wie diese Funktion es seit der Einführung optimierter Frames getan hat.
Da der Frame-Value-Cache sowieso erhalten bleibt, machte es weiter Sinn, ihn zu nutzen, um die Implementierung des schnellen Locals-Proxy-Mappings zu vereinfachen.
Hinweis: Die Tatsache, dass PEP 667 den internen Frame-Value-Cache nicht als Teil der Write-Through-Proxy-Implementierung verwendet, ist der wichtigste Unterschied auf Python-Ebene zwischen den beiden PEPs.
Änderung der Semantik der Frame-API im regulären Betrieb
Hinweis: Als diese PEP ursprünglich geschrieben wurde, lag sie vor der Änderung in Python 3.11, die die implizite Rückschreibung der lokalen Frame-Variablen entfernte, sobald ein Tracing-Hook installiert war. Daher wurde die Aufnahme dieser Änderung als Teil des Vorschlags aufgenommen.
Frühere Versionen dieser PEP schlugen vor, die Semantik des Frame-Attributs f_locals davon abhängig zu machen, ob ein Tracing-Hook installiert war oder nicht – nur das Write-Through-Proxy-Verhalten bereitzustellen, wenn ein Tracing-Hook aktiv war, und ansonsten wie der historische eingebaute Befehl locals() zu agieren.
Dies wurde aus zwei Hauptgründen als ursprünglicher Designvorschlag übernommen, einem pragmatischen und einem eher philosophischen:
- Objektallokationen und Methodenhüllen sind nicht kostenlos, und Tracing-Hooks sind nicht die einzigen Operationen, die von außerhalb der Funktion auf Frame-Locals zugreifen. Die Beschränkung der Änderungen auf den Tracing-Modus bedeutete, dass die zusätzlichen Speicher- und Ausführungszeitkosten dieser Änderungen im regulären Betrieb so nah wie möglich bei Null liegen würden.
- "Ändere nicht, was nicht kaputt ist": Die aktuellen Probleme im Tracing-Modus werden durch eine Anforderung verursacht, die spezifisch für den Tracing-Modus ist (Unterstützung für externe Neubindung von Funktions-Lokale-Variablen-Referenzen), daher war es sinnvoll, verwandte Korrekturen ebenfalls auf den Tracing-Modus zu beschränken.
Tatsächlich hat der Versuch, diesen dynamischen Ansatz zu implementieren und zu dokumentieren, jedoch die Tatsache hervorgehoben, dass dies eine sehr subtile, laufzeitzustandsabhängige Verhaltensunterscheidung bei der Funktionsweise von frame.f_locals bewirkt und mehrere neue Grenzfälle schafft, wie f_locals beim Hinzufügen und Entfernen von Trace-Funktionen funktioniert.
Daher wurde das Design auf das aktuelle umgestellt, bei dem frame.f_locals immer ein Write-Through-Proxy ist und locals() immer ein Schnappschuss ist, was sowohl einfacher zu implementieren als auch leichter zu erklären ist.
Unabhängig davon, wie die CPython-Referenzimplementierung dies handhabt, dürfen optimierende Compiler und Interpreter Debuggern auch weiterhin zusätzliche Beschränkungen auferlegen, wie z. B. dass die Mutation lokaler Variablen über Frame-Objekte ein Opt-in-Verhalten ist, das einige Optimierungen deaktivieren kann (genauso wie die Emulation der CPython-Frame-API bereits ein Opt-in-Flag in einigen Python-Implementierungen ist).
Fortgesetzte Unterstützung für die Speicherung zusätzlicher Daten auf optimierten Frames
Eine der Entwurfsiterationen dieses PEP schlug vor, die Möglichkeit zu entfernen, zusätzliche Daten auf optimierten Frames zu speichern, indem auf frame.f_locals Schlüssel geschrieben wird, die keinen lokalen Variablen- oder Closure-Variablennamen im zugrunde liegenden Frame entsprechen.
Obwohl diese Idee eine attraktive Vereinfachung der Implementierung des Fast-Locals-Proxys bot, speichert pdb __return__ und __exception__ Werte auf beliebigen Frames, sodass die Standardbibliothekstestsuite fehlschlägt, wenn diese Funktionalität nicht mehr funktioniert.
Daher wurde die Möglichkeit, beliebige Schlüssel zu speichern, beibehalten, auf Kosten einiger Operationen auf Proxy-Objekten, die langsamer sind, als sie sonst sein könnten (da sie nicht davon ausgehen können, dass nur im Codeobjekt definierte Namen über den Proxy zugänglich sind).
Es wird erwartet, dass sich die genauen Details der Interaktion zwischen dem Fast-Locals-Proxy und dem f_locals Werte-Cache auf dem zugrunde liegenden Frame im Laufe der Zeit weiterentwickeln, sobald Verbesserungsmöglichkeiten identifiziert werden.
Historische Semantik im Funktions-Scope
Die aktuellen Semantiken für die Mutation von locals() und frame.f_locals in CPython sind aufgrund historischer Implementierungsdetails ziemlich eigenartig.
- Die tatsächliche Ausführung verwendet das Fast-Locals-Array für Bindungen lokaler Variablen und Zellreferenzen für nicht-lokale Variablen.
- Es gibt eine
PyFrame_FastToLocals-Operation, die dasf_locals-Attribut des Frames basierend auf dem aktuellen Zustand des Fast-Locals-Arrays und aller referenzierten Zellen füllt. Dies existiert aus drei Gründen:- ermöglicht Trace-Funktionen das Lesen des Zustands lokaler Variablen
- ermöglicht Traceback-Prozessoren das Lesen des Zustands lokaler Variablen
- ermöglicht
locals()das Lesen des Zustands lokaler Variablen
- eine direkte Referenz auf
frame.f_localswird vonlocals()zurückgegeben. Wenn Sie also mehrere gleichzeitige Referenzen übergeben, werden sich alle diese Referenzen auf dasselbe Wörterbuch beziehen. - die beiden häufigen Aufrufe der umgekehrten Operation,
PyFrame_LocalsToFast, wurden bei der Migration zu Python 3 entfernt:execist keine Anweisung mehr (und kann daher keine Funktionslokal-Namensräume mehr beeinflussen), und der Compiler verbietet nun die Verwendung vonfrom module import .*-Operationen im Funktionsumfang. - Es bleiben jedoch zwei obskure Aufrufwege erhalten:
PyFrame_LocalsToFastwird als Teil der Rückgabe aus einer Trace-Funktion aufgerufen (was es Debuggern ermöglicht, Änderungen am Zustand lokaler Variablen vorzunehmen), und Sie können denIMPORT_STAR-Opcode auch noch injizieren, wenn Sie eine Funktion direkt aus einem Codeobjekt und nicht über den Compiler erstellen.
Dieser Vorschlag formalisiert diese Semantiken absichtlich *nicht* so, wie sie sind, da sie nur im Kontext der historischen Entwicklung der Sprache und der Referenzimplementierung Sinn ergeben und nicht absichtlich entworfen wurden.
Vorschlag mehrerer Ergänzungen zur stabilen C API/ABI
Historisch gesehen hat die CPython C-API (und später die Stable ABI) nur eine einzige API-Funktion im Zusammenhang mit der Python-Builtin-Funktion locals bereitgestellt: PyEval_GetLocals(). Da sie jedoch eine geliehene Referenz zurückgibt, ist es nicht möglich, diese Schnittstelle direkt für die Unterstützung der in diesem PEP vorgeschlagenen neuen locals()-Semantiken anzupassen.
Eine frühere Iteration dieses PEP schlug eine minimalistische Anpassung an die neuen Semantiken vor: eine C-API-Funktion, die sich wie die Python-Builtin-Funktion locals() verhält, und eine weitere, die sich wie der frame.f_locals-Deskriptor verhält (wobei bei Bedarf der Write-Through-Proxy erstellt und zurückgegeben wird).
Das Feedback [8] zu dieser Version der C-API war, dass sie zu stark auf die Implementierung der Semantiken auf Python-Ebene basierte und die Verhaltensweisen, die Autoren von C-Erweiterungen wahrscheinlich *benötigen*, nicht berücksichtigte.
Die breitere API, die nun vorgeschlagen wird, entstand aus der Gruppierung der potenziellen Gründe für den Zugriff auf den locals()-Namensraum von Python aus einem Erweiterungsmodul in die folgenden Fälle:
- die Notwendigkeit, die Semantiken der Python-Level-Operation
locals()exakt zu replizieren. Dies ist diePyLocals_Get()API. - die Notwendigkeit, sich unterschiedlich zu verhalten, je nachdem, ob Schreibvorgänge auf das Ergebnis von
PyLocals_Get()für Python-Code sichtbar sind oder nicht. Dies wird durch die Abfrage-APIPyLocals_GetKind()abgedeckt. - immer einen veränderbaren Namensraum wünschen, der aus dem aktuellen
locals()-Namensraum von Python vorab gefüllt wurde, aber *keine* Änderungen für Python-Code sichtbar sein sollen. Dies ist diePyLocals_GetCopy()API. - immer eine schreibgeschützte Ansicht des aktuellen Locals-Namensraums wünschen, ohne den Laufzeitaufwand einer vollständigen Kopie jedes Mal in Kauf zu nehmen. Dies wird für optimierte Frames aufgrund der Notwendigkeit, zu prüfen, ob Namen derzeit gebunden sind oder nicht, nicht ohne Weiteres angeboten, daher wird keine spezielle API hinzugefügt, um dies abzudecken.
Historisch gesehen wären diese Arten von Überprüfungen und Operationen nur möglich gewesen, wenn eine Python-Implementierung die vollständige CPython-Frame-API emuliert hätte. Mit der vorgeschlagenen API können Erweiterungsmodule stattdessen klarer nach den Semantiken fragen, die sie tatsächlich benötigen, und Python-Implementierungen mehr Flexibilität bei der Bereitstellung dieser Fähigkeiten geben.
Vergleich mit PEP 667
HINWEIS: Der Vergleich unten bezieht sich auf PEP 667, wie er im Dezember 2021 war. Er spiegelt nicht den Zustand von PEP 667 im April 2024 wider (als dieser PEP zugunsten von PEP 667 zurückgezogen wurde).
PEP 667 bietet einen teilweise konkurrierenden Vorschlag für diesen PEP, der vorschlägt, dass es angemessen wäre, den internen Frame-Wert-Cache auf optimierten Frames vollständig zu eliminieren.
Diese Änderungen wurden ursprünglich als Ergänzungen zu PEP 558 angeboten, und der PEP-Autor lehnte sie aus drei Hauptgründen ab:
- Die anfängliche Behauptung, dass
PyEval_GetLocals()nicht reparierbar sei, da sie eine geliehene Referenz zurückgibt, war schlichtweg falsch, da sie in der Referenzimplementierung von PEP 558 weiterhin funktioniert. Alles, was erforderlich ist, um sie weiterhin zum Laufen zu bringen, ist die Beibehaltung des internen Frame-Wert-Caches und die Gestaltung des Fast-Locals-Proxys so, dass er den Cache mit Änderungen im Frame-Zustand auf relativ einfache Weise auf dem neuesten Stand hält, ohne erhebliche Laufzeitkosten zu verursachen, wenn der Cache nicht benötigt wird. Da diese Behauptung falsch ist, schlägt der Vorschlag, dass alle Code, der diePyEval_GetLocals()API verwendet, neu geschrieben werden muss, um eine neue API mit anderen Refcounting-Semantiken zu verwenden, PEP 387's Anforderung, dass API-Kompatibilitätsbrüche ein großes Verhältnis von Nutzen zu Bruch haben sollten (da das Weglassen des Caches keinen signifikanten Nutzen bringt und kein Codebruch gerechtfertigt werden kann), fehl. Die einzige wirklich nicht reparierbare öffentliche API istPyFrame_LocalsToFast()(weshalb beide PEPs den Bruch dieser vorschlagen). - Ohne eine Form des internen Wert-Caches werden die Leistungseigenschaften der Fast-Locals-Proxy-Zuordnung des Fast-Locals-Proxys recht unintuitiv.
len(proxy)wird beispielsweise konsistent O(n) in Bezug auf die Anzahl der im Frame definierten Variablen, da der Proxy das gesamte Fast-Locals-Array durchlaufen muss, um zu sehen, welche Namen derzeit an Werte gebunden sind, bevor er die Antwort bestimmen kann. Im Gegensatz dazu ermöglicht die Beibehaltung eines internen Frame-Wert-Caches, dass Proxys in Bezug auf die algorithmische Komplexität weitgehend als normale Wörterbücher behandelt werden können, wobei nur die anfängliche implizite O(n)-Cache-Aktualisierung berücksichtigt werden muss, die beim ersten Ausführen einer Operation ausgeführt wird, die auf einem aktuellen Cache basiert. - Die Behauptung, dass eine cachefreie Implementierung einfacher wäre, ist höchst fragwürdig, da PEP 667 nur eine reine Python-Skizze eines Teils einer veränderbaren Mapping-Implementierung enthält, anstatt einer vollwertigen C-Implementierung eines neuen Mapping-Typs, der mit der zugrunde liegenden Datenspeicherung für optimierte Frames integriert ist. Die Implementierung des Fast-Locals-Proxys von PEP 558 delegiert stark an den Frame-Wert-Cache für die Operationen, die zur vollständigen Implementierung der veränderbaren Mapping-API erforderlich sind, und ermöglicht so die Wiederverwendung der vorhandenen Dict-Implementierungen der folgenden Operationen:
__len____str____or__(Dict-Vereinigung)__iter__(ermöglicht die Wiederverwendung desdict_keyiterator-Typs)__reversed__(ermöglicht die Wiederverwendung desdict_reversekeyiterator-Typs)keys()(ermöglicht die Wiederverwendung desdict_keys-Typs)values()(ermöglicht die Wiederverwendung desdict_values-Typs)items()(ermöglicht die Wiederverwendung desdict_items-Typs)copy()popitem()- Wertvergleichsoperationen
Von den drei Gründen ist der erste der wichtigste (da wir überzeugende Gründe für die Brechung der API-Abwärtskompatibilität benötigen, und die haben wir nicht).
Nach Überprüfung der Python-Level-Semantiken, die in PEP 667 vorgeschlagen werden, stimmte der Autor dieses PEP schließlich zu, dass sie für Benutzer der Python locals() API einfacher *wären*. Daher wurde diese Unterscheidung zwischen den beiden PEPs beseitigt: Unabhängig davon, welcher PEP und welche Implementierung akzeptiert wird, bietet das Fast-Locals-Proxy-Objekt *immer* eine konsistente Ansicht des aktuellen Zustands der lokalen Variablen, auch wenn dies dazu führt, dass einige Operationen O(n) werden, die bei einem regulären Wörterbuch O(1) wären (insbesondere wird len(proxy) O(n), da es prüfen muss, welche Namen derzeit gebunden sind, und Proxy-Mapping-Vergleiche vermeiden die Längenprüfungs-Optimierung, die Unterschiede in der Anzahl der gespeicherten Schlüssel schnell für reguläre Mappings erkennen lässt).
Aufgrund der Übernahme dieser nicht standardmäßigen Leistungseigenschaften in der Proxy-Implementierung wurden die C-APIs PyLocals_GetView() und PyFrame_GetLocalsView() ebenfalls aus dem Vorschlag in diesem PEP entfernt.
Dies lässt die einzigen verbleibenden Unterscheidungspunkte zwischen den beiden PEPs als spezifisch für die C-API:
- PEP 667 schlägt immer noch völlig unnötige C-API-Unterbrechungen vor (die programmatische Deprekation und eventuelle Entfernung von
PyEval_GetLocals(),PyFrame_FastToLocalsWithError()undPyFrame_FastToLocals()) ohne Begründung, obwohl es durchaus möglich ist, diese auf unbestimmte Zeit (und interoperabel) funktionsfähig zu halten, vorausgesetzt, eine entsprechend gestaltete Fast-Locals-Proxy-Implementierung. - Die Fast-Locals-Proxy-Handhabung von zusätzlichen Variablen wird in diesem PEP so definiert, dass sie vollständig mit der bestehenden
PyEval_GetLocals()API interoperabel ist. In der in PEP 667 vorgeschlagenen Proxy-Implementierung sehen Benutzer der neuen Frame-API keine Änderungen an zusätzlichen Variablen, die von Benutzern der alten API vorgenommen wurden, und Änderungen an zusätzlichen Variablen, die über die alte API vorgenommen wurden, werden bei nachfolgenden Aufrufen vonPyEval_GetLocals()überschrieben. - Die
PyLocals_Get()API in diesem PEP wird in PEP 667 alsPyEval_Locals()bezeichnet. Dieser Funktionsname ist etwas seltsam, da er kein Verb enthält, was ihn eher wie einen Typnamen als eine Datenzugriffs-API erscheinen lässt. - Dieser PEP fügt die APIs
PyLocals_GetCopy()undPyFrame_GetLocalsCopy()hinzu, um Erweiterungsmodulen die einfache Vermeidung eines doppelten Kopiervorgangs in Frames zu ermöglichen, bei denenPyLocals_Get()bereits eine Kopie erstellt. - Dieser PEP fügt
PyLocals_Kind,PyLocals_GetKind()undPyFrame_GetLocalsKind()hinzu, um Erweiterungsmodulen zu ermöglichen, zu identifizieren, wann Code im Funktionsumfang ausgeführt wird, ohne nicht-portable Frame- und Codeobjekt-APIs inspizieren zu müssen (ohne die vorgeschlagene Abfrage-API ist das bestehende Äquivalent zur neuen ÜberprüfungPyLocals_GetKind() == PyLocals_SHALLOW_COPYdie Einbindung der internen CPython-Frame-API-Header und die Überprüfung, ob_PyFrame_GetCode(PyEval_GetFrame())->co_flags & CO_OPTIMIZEDgesetzt ist).
Der unten stehende Python-Pseudocode basiert auf der Implementierungsskizze, die in PEP 667 zum Zeitpunkt des Schreibens (24.10.2021) vorgestellt wurde. Die Unterschiede, die die verbesserte Interoperabilität zwischen der neuen Fast-Locals-Proxy-API und der bestehenden API PyEval_GetLocals() bieten, werden in Kommentaren vermerkt.
Wie in PEP 667 sind alle Attribute, die mit einem Unterstrich beginnen, unsichtbar und können nicht direkt zugegriffen werden. Sie dienen nur zur Veranschaulichung des vorgeschlagenen Designs.
Der Einfachheit halber (und wie in PEP 667) wird die Handhabung von Modul- und Klassenebenen-Frames ausgelassen (sie sind viel einfacher, da _locals der Ausführungsnamensraum *ist*, sodass keine Übersetzung erforderlich ist).
NULL: Object # NULL is a singleton representing the absence of a value.
class CodeType:
_name_to_offset_mapping_impl: dict | NULL
...
def __init__(self, ...):
self._name_to_offset_mapping_impl = NULL
self._variable_names = deduplicate(
self.co_varnames + self.co_cellvars + self.co_freevars
)
...
def _is_cell(self, offset):
... # How the interpreter identifies cells is an implementation detail
@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:
_fast_locals : array[Object] # The values of the local variables, items may be NULL.
_locals: dict | NULL # Dictionary returned by PyEval_GetLocals()
def __init__(self, ...):
self._locals = NULL
...
@property
def f_locals(self):
return FastLocalsProxy(self)
class FastLocalsProxy:
__slots__ "_frame"
def __init__(self, frame:FrameType):
self._frame = frame
def _set_locals_entry(self, name, val):
f = self._frame
if f._locals is NULL:
f._locals = {}
f._locals[name] = val
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._fast_locals[index]
if val is NULL:
raise KeyError(name)
if co._is_cell(offset)
val = val.cell_contents
if val is NULL:
raise KeyError(name)
# PyEval_GetLocals() interop: implicit frame cache refresh
self._set_locals_entry(name, val)
return val
# PyEval_GetLocals() interop: frame cache may contain additional names
if f._locals is NULL:
raise KeyError(name)
return f._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 co._is_cell(offset)
cell = f._locals[index]
cell.cell_contents = val
else:
f._fast_locals[index] = val
# PyEval_GetLocals() interop: implicit frame cache update
# even for names that are part of the fast locals array
self._set_locals_entry(name, val)
def __delitem__(self, name):
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 co._is_cell(offset)
cell = f._locals[index]
cell.cell_contents = NULL
else:
f._fast_locals[index] = NULL
# PyEval_GetLocals() interop: implicit frame cache update
# even for names that are part of the fast locals array
if f._locals is not NULL:
del f._locals[name]
def __iter__(self):
f = self._frame
co = f.f_code
for index, name in enumerate(co._variable_names):
val = f._fast_locals[index]
if val is NULL:
continue
if co._is_cell(offset):
val = val.cell_contents
if val is NULL:
continue
yield name
for name in f._locals:
# Yield any extra names not defined on the frame
if name in co._name_to_offset_mapping:
continue
yield name
def popitem(self):
f = self._frame
co = f.f_code
for name in self:
val = self[name]
# PyEval_GetLocals() interop: implicit frame cache update
# even for names that are part of the fast locals array
del name
return name, val
def _sync_frame_cache(self):
# This method underpins PyEval_GetLocals, PyFrame_FastToLocals
# PyFrame_GetLocals, PyLocals_Get, mapping comparison, etc
f = self._frame
co = f.f_code
res = 0
if f._locals is NULL:
f._locals = {}
for index, name in enumerate(co._variable_names):
val = f._fast_locals[index]
if val is NULL:
f._locals.pop(name, None)
continue
if co._is_cell(offset):
if val.cell_contents is NULL:
f._locals.pop(name, None)
continue
f._locals[name] = val
def __len__(self):
self._sync_frame_cache()
return len(self._locals)
Hinweis: Die einfachste Möglichkeit, die früheren Iterationen der Referenzimplementierung von PEP 558 in eine vorläufige Implementierung der nun vorgeschlagenen Semantiken umzuwandeln, besteht darin, die frame_cache_updated-Prüfungen in den betroffenen Operationen zu entfernen und stattdessen immer den Frame-Cache in diesen Methoden zu synchronisieren. Die Übernahme dieses Ansatzes ändert die algorithmische Komplexität der folgenden Operationen wie gezeigt (wobei n die Anzahl der lokalen und zellulären Variablen ist, die auf dem Frame definiert sind):
__len__: O(1) -> O(n)- Wertvergleichsoperationen: profitieren nicht mehr von der O(1)-Längenprüfungs-Kurzform.
__iter__: O(1) -> O(n)__reversed__: O(1) -> O(n)keys(): O(1) -> O(n)values(): O(1) -> O(n)items(): O(1) -> O(n)popitem(): O(1) -> O(n)
Die Längenprüfungs- und Wertvergleichsoperationen haben relativ begrenzte Verbesserungsmöglichkeiten: Ohne die Nutzung eines potenziell veralteten Caches ist die einzige Möglichkeit, die Anzahl der derzeit gebundenen Variablen zu erfahren, sie alle zu durchlaufen und zu prüfen. Wenn die Implementierung ohnehin so viele Zyklen für eine Operation aufwendet, könnte sie genauso gut die Frame-Wert-Aktualisierung durchführen und dann das Ergebnis verbrauchen. Diese Operationen sind sowohl in diesem PEP als auch in PEP 667 O(n). Benutzerdefinierte Implementierungen könnten bereitgestellt werden, die schneller sind als die Aktualisierung des Frame-Caches, aber es ist keineswegs klar, dass die zusätzliche Codekomplexität, die zur Beschleunigung dieser Operationen erforderlich ist, lohnenswert wäre, wenn sie nur eine lineare Leistungsverbesserung und keine Verbesserung der algorithmischen Komplexität bietet.
Die O(1)-Natur der anderen Operationen kann durch Hinzufügen von Implementierungscode, der nicht auf einem aktuellen Wert-Cache beruht, wiederhergestellt werden.
Die Beibehaltung der Iterator/Iterable-Abrufmethoden als O(1) erfordert das Schreiben benutzerdefinierter Ersetzungen für die entsprechenden integrierten Dict-Hilfstypen, genau wie in PEP 667 vorgeschlagen. Wie oben gezeigt, wären die Implementierungen ähnlich dem Pseudocode in PEP 667, aber nicht identisch (aufgrund der verbesserten Interoperabilität von PyEval_GetLocals(), die dieser PEP bietet und die sich auf die Art und Weise auswirkt, wie zusätzliche Variablen gespeichert werden).
popitem() kann von "immer O(n)" zu "O(n) im schlimmsten Fall" verbessert werden, indem eine benutzerdefinierte Implementierung erstellt wird, die auf den verbesserten Iterator-APIs basiert.
Um sicherzustellen, dass veraltete Frame-Informationen niemals in der Python Fast-Locals-Proxy-API präsentiert werden, müssen diese Änderungen in der Referenzimplementierung vor dem Merging umgesetzt werden.
Die aktuelle Implementierung zum Zeitpunkt des Schreibens (24.10.2021) speichert außerdem immer noch eine Kopie der Fast-Refs-Zuordnung auf jedem Frame, anstatt eine einzelne Instanz auf dem zugrunde liegenden Codeobjekt zu speichern (da sie Zellreferenzen immer noch direkt speichert, anstatt bei jedem Zugriff auf das Fast-Locals-Array nach Zellen zu suchen). Die Behebung dieses Problems wäre ebenfalls vor dem Merging erforderlich.
Implementierung
Die Aktualisierung der Referenzimplementierung befindet sich als Entwurf eines Pull-Requests auf GitHub in Entwicklung ([6]).
Danksagungen
Vielen Dank an Nathaniel J. Smith für den Vorschlag der Write-Through-Proxy-Idee in [1] und für die Aufdeckung einiger kritischer Designfehler in früheren Iterationen des PEPs, die versuchten, einen solchen Proxy zu vermeiden.
Vielen Dank an Steve Dower und Petr Viktorin dafür, dass sie auf eine stärkere Berücksichtigung der Entwicklererfahrung der vorgeschlagenen C-API-Ergänzungen hingewiesen haben [8] [13].
Vielen Dank an Larry Hastings für den Vorschlag, wie Enums in der Stable ABI verwendet werden können und gleichzeitig sichergestellt wird, dass sie sicher von beliebigen ganzen Zahlen in Typen umgewandelt werden können.
Vielen Dank an Mark Shannon dafür, dass er sich für eine weitere Vereinfachung der C-Level-API und Semantiken eingesetzt hat, sowie für eine erhebliche Klarstellung des PEP-Textes (und dafür, dass er die Diskussion über den PEP Anfang 2021 nach einem weiteren Jahr der Inaktivität wieder aufgenommen hat) [10] [11] [12]. Marks Kommentare, die schließlich als PEP 667 veröffentlicht wurden, führten auch direkt zu mehreren Effizienzverbesserungen, die die Kosten redundanter O(n) Mapping-Aktualisierungsoperationen vermeiden, wenn die relevanten Mappings nicht verwendet werden, sowie zu der Änderung, um sicherzustellen, dass der über die Python-Level f_locals API gemeldete Zustand niemals veraltet ist.
Referenzen
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-0558.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT