Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie jetzt unter locals().

×

Siehe PEP 1, um Änderungen vorzuschlagen.

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 wie frame.f_locals)
  • dass locals() wirklich unabhängige Schnappschüsse zurückgibt, sodass Versuche, die Werte lokaler Variablen über exec() 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_locals bietet 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.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0667.rst

Zuletzt geändert: 2024-10-27 07:11:46 GMT