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

Python Enhancement Proposals

PEP 749 – Implementierung von PEP 649

Autor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Benötigt:
649
Erstellt:
28-Mai-2024
Python-Version:
3.14
Post-History:
04-Jun-2024
Resolution:
05-Mai-2025

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie nun unter Annotationen und annotationlib.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Dieses PEP ergänzt PEP 649 durch verschiedene Anpassungen und Ergänzungen zu seiner Spezifikation

  • from __future__ import annotations (PEP 563) wird mit seinem aktuellen Verhalten mindestens bis zum Ende der Lebensdauer von Python 3.13 fortbestehen. Anschließend wird es veraltet sein und schließlich entfernt werden.
  • Ein neues Modul der Standardbibliothek, annotationlib, wird hinzugefügt, um Werkzeuge für Annotationen bereitzustellen. Es wird die Funktion get_annotations(), ein Enum für Annotationsformate, eine ForwardRef Klasse und eine Hilfsfunktion zum Aufrufen von __annotate__ Funktionen enthalten.
  • Annotationen in der REPL werden verzögert ausgewertet, genau wie andere Annotationen auf Modulebene.
  • Wir spezifizieren das Verhalten von Wrapper-Objekten, die Annotationen bereitstellen, wie z. B. classmethod() und Code, der functools.wraps() verwendet.
  • Es wird kein Code-Flag zum Markieren von __annotate__ Funktionen geben, die in einer „Fake Globals“-Umgebung ausgeführt werden können. Stattdessen fügen wir ein viertes Format, VALUE_WITH_FAKE_GLOBALS, hinzu, um Drittanbieter-Implementierern von Annotationsfunktionen die Angabe der unterstützten Formate zu ermöglichen.
  • Das direkte Löschen des Attributs __annotations__ löscht auch __annotate__.
  • Wir fügen Funktionalität hinzu, um die Auswertung von Typ-Alias-Werten und Typ-Parameter-Grenzen und Standardwerten (die durch PEP 695 und PEP 696 hinzugefügt wurden) mit PEP 649-ähnlichen Semantiken zu ermöglichen.
  • Das Format SOURCE wird in STRING umbenannt, um die Klarheit zu verbessern und das Risiko von Benutzerverwirrung zu verringern.
  • Bedingt definierte Klassen- und Modulannotationen werden korrekt behandelt.
  • Wenn Annotationen auf einem teilweise ausgeführten Modul zugegriffen werden, werden die bisher ausgeführten Annotationen zurückgegeben, aber nicht zwischengespeichert.

Motivation

PEP 649 bietet ein ausgezeichnetes Framework für die Erstellung besserer Semantiken für Annotationen in Python. Es löst einen häufigen Schmerzpunkt für Benutzer von Annotationen, einschließlich derjenigen, die statische Typ-Hints sowie Laufzeit-Typisierung verwenden, und macht die Sprache eleganter und leistungsfähiger. Das PEP wurde ursprünglich 2021 für Python 3.10 vorgeschlagen und 2023 angenommen. Die Implementierung dauerte jedoch länger als erwartet, und nun wird erwartet, dass das PEP in Python 3.14 implementiert wird.

Ich habe mit der Implementierung des PEP in CPython begonnen. Ich stellte fest, dass das PEP einige Bereiche unterdefiniert lässt und einige seiner Entscheidungen in Eckfällen fragwürdig sind. Dieses neue PEP schlägt mehrere Änderungen und Ergänzungen zur Spezifikation vor, um diese Probleme zu beheben.

Dieses PEP ergänzt PEP 649, anstatt es zu ersetzen. Die hier vorgeschlagenen Änderungen sollen die allgemeine Benutzererfahrung verbessern, ändern jedoch nicht den allgemeinen Rahmen des früheren PEPs.

Die Zukunft von from __future__ import annotations

PEP 563 führte zuvor den Future-Import from __future__ import annotations ein, der alle Annotationen in Strings umwandelt. PEP 649 schlägt einen alternativen Ansatz vor, der diesen Future-Import nicht erfordert, und besagt

Wenn dieses PEP angenommen wird, wird PEP 563 veraltet und schließlich entfernt.

Das PEP liefert jedoch keinen detaillierten Plan für diese Veraltung.

Es gibt einige frühere Diskussionen zu diesem Thema auf Discourse (beachten Sie, dass ich im verlinkten Beitrag etwas anderes vorgeschlagen habe als hier).

Spezifikation

Wir schlagen den folgenden Veraltungsplan vor

  • In Python 3.14 funktioniert from __future__ import annotations weiterhin wie zuvor und wandelt Annotationen in Strings um.
    • Wenn der Future-Import aktiv ist, gibt die Funktion __annotate__ von Objekten mit Annotationen die Annotationen als Strings zurück, wenn sie mit dem Format VALUE aufgerufen wird, was dem Verhalten von __annotations__ entspricht.
  • Irgendwann nach dem Ende der Lebensdauer der letzten Version, die PEP 649 Semantiken nicht unterstützte (erwartet wird 3.13), wird from __future__ import annotations veraltet sein. Das Kompilieren von Code, der den Future-Import verwendet, gibt eine DeprecationWarning aus. Dies geschieht nicht früher als in der ersten Version nach dem Ende der Lebensdauer von Python 3.13, aber die Community kann entscheiden, länger zu warten.
  • Nach mindestens zwei Versionen wird der Future-Import entfernt und Annotationen werden immer gemäß PEP 649 ausgewertet. Code, der weiterhin den Future-Import verwendet, löst einen SyntaxError aus, ähnlich wie bei jedem anderen undefinierten Future-Import.

Abgelehnte Alternativen

Den Future-Import sofort zu einem No-Op machen: Wir haben erwogen, die PEP 649 Semantiken in Python 3.14 auf allen Code anzuwenden und den Future-Import zu einem No-Op zu machen. Dies würde jedoch Code brechen, der in 3.13 unter den folgenden Bedingungen funktioniert

  • __future__ import annotations ist aktiv
  • Es gibt Annotationen, die auf Vorwärtsreferenzen angewiesen sind
  • Annotationen werden sofort beim Import ausgewertet, z. B. durch eine Metaklasse oder einen Klassen- oder Funktionsdecorator. Dies gilt derzeit beispielsweise für die veröffentlichte Version von typing_extensions.TypedDict.

Dies wird voraussichtlich ein gängiges Muster sein, daher können wir es uns nicht leisten, solchen Code beim Upgrade von 3.13 auf 3.14 zu brechen.

Solcher Code würde weiterhin brechen, wenn der Future-Import schließlich entfernt wird. Dies liegt jedoch viele Jahre in der Zukunft, was betroffenen Bibliotheken genügend Zeit gibt, ihren Code zu aktualisieren.

Den Future-Import sofort veraltet erklären: Anstatt bis zum Ende der Lebensdauer von Python 3.13 zu warten, könnten wir sofort Warnungen ausgeben, wenn der Future-Import verwendet wird. Viele Bibliotheken verwenden jedoch bereits from __future__ import annotations als elegante Methode, um uneingeschränkte Vorwärtsreferenzen in ihren Annotationen zu ermöglichen. Wenn wir den Future-Import sofort veraltet erklären würden, wäre es für diese Bibliotheken unmöglich, uneingeschränkte Vorwärtsreferenzen auf allen unterstützten Python-Versionen zu verwenden und gleichzeitig Veraltungswarnungen zu vermeiden: Im Gegensatz zu anderen aus der Standardbibliothek veralteten Funktionen muss ein __future__-Import die erste Anweisung in einem bestimmten Modul sein, was bedeutet, dass es unmöglich wäre, __future__.annotations nur bedingt auf Python 3.13 und niedriger zu importieren. (Die notwendige Überprüfung von sys.version_info würde als Anweisung vor dem __future__-Import zählen.)

Den Future-Import für immer behalten: Wir könnten uns auch entscheiden, den Future-Import auf unbestimmte Zeit beizubehalten. Dies würde jedoch das Verhalten der Python-Sprache dauerhaft spalten. Das ist unerwünscht; die Sprache sollte nur einen einzigen Satz von Semantiken haben, nicht zwei dauerhaft unterschiedliche Modi.

Den Future-Import in Zukunft zu einem No-Op machen: Anstatt from __future__ import annotations irgendwann nach dem Ende der Lebensdauer von Python 3.13 zu einem SyntaxError zu machen, könnten wir ihn stattdessen zu nichts machen lassen. Dies hat immer noch einige der gleichen Probleme wie oben erwähnt, wenn es sofort zu einem No-Op gemacht wird, obwohl das Ökosystem viel länger Zeit gehabt hätte, sich anzupassen. Es ist besser, wenn Benutzer den Future-Import später explizit aus ihrem Code entfernen, sobald sie bestätigt haben, dass sie sich nicht auf String-basierte Annotationen verlassen.

Neues Modul annotationlib

PEP 649 schlägt vor, Werkzeuge im Zusammenhang mit Annotationen zum Modul inspect hinzuzufügen. Dieses Modul ist jedoch recht groß, hat direkte oder indirekte Abhängigkeiten von mindestens 35 anderen Modulen der Standardbibliothek und ist so langsam zu importieren, dass andere Module der Standardbibliothek oft davon abgeraten werden, es zu importieren. Außerdem erwarten wir, dass wir neben der Funktion inspect.get_annotations() und den Formaten VALUE, FORWARDREF und SOURCE weitere Werkzeuge hinzufügen.

Ein neues Modul der Standardbibliothek bietet eine logische Heimat für diese Funktionalität und ermöglicht es uns auch, weitere Werkzeuge hinzuzufügen, die für Konsumenten von Annotationen nützlich sind.

Begründung

PEP 649 gibt an, dass typing.ForwardRef verwendet werden sollte, um das Format FORWARDREF in inspect.get_annotations() zu implementieren. Die bestehende Implementierung von typing.ForwardRef ist jedoch mit dem Rest des typing-Moduls verknüpft, und es wäre nicht sinnvoll, typing-spezifisches Verhalten zur generischen Funktion get_annotations() hinzuzufügen. Darüber hinaus ist typing.ForwardRef eine problematische Klasse: Sie ist öffentlich und dokumentiert, aber die Dokumentation listet keine Attribute oder Methoden für sie auf. Dennoch nutzen Drittanbieterbibliotheken einige ihrer undokumentierten Attribute. Zum Beispiel verwenden Pydantic und Typeguard die Methode _evaluate; beartype und pyanalyze verwenden das Attribut __forward_arg__.

Wir ersetzen die bestehende, aber schlecht spezifizierte Klasse typing.ForwardRef durch eine neue Klasse, annotationlib.ForwardRef. Sie ist so konzipiert, dass sie weitgehend mit bestehenden Verwendungen der Klasse typing.ForwardRef kompatibel ist, jedoch ohne die spezifischen Verhaltensweisen des typing-Moduls. Zur Kompatibilität mit bestehenden Benutzern behalten wir die private Methode _evaluate bei, markieren sie aber als veraltet. Sie leitet an eine neue öffentliche Funktion im typing-Modul, typing.evaluate_forward_ref, weiter, die dazu dient, Vorwärtsreferenzen spezifisch für Typ-Hints auszuwerten.

Wir fügen die Funktion annotationlib.call_annotate_function als Hilfsmittel zum Aufrufen von __annotate__ Funktionen hinzu. Dies ist ein nützlicher Baustein bei der Implementierung von Funktionalität, die Annotationen teilweise auswerten muss, während eine Klasse konstruiert wird. Zum Beispiel muss die Implementierung von typing.NamedTuple die Annotationen aus einem Klassennamen-Dictionary abrufen, bevor die Namedtuple-Klasse selbst konstruiert werden kann, da die Annotationen bestimmen, welche Felder auf dem Namedtuple vorhanden sind.

Spezifikation

Ein neues Modul, annotationlib, wird der Standardbibliothek hinzugefügt. Sein Ziel ist es, Werkzeuge zur Introspektion und zum Wrapper von Annotationen bereitzustellen.

Das Design des Moduls wird durch die Erfahrung bei der Aktualisierung der Standardbibliothek (z. B. dataclasses und typing.TypedDict) für die Verwendung von PEP 649 Semantiken geprägt.

Das Modul wird folgende Funktionalität enthalten

  • get_annotations(): Eine Funktion, die die Annotationen einer Funktion, eines Moduls oder einer Klasse zurückgibt. Dies wird inspect.get_annotations() ersetzen. Letztere wird an die neue Funktion delegieren. Sie könnte irgendwann veraltet sein, aber um Störungen zu minimieren, schlagen wir keine sofortige Veraltung vor.
  • get_annotate_from_class_namespace(namespace: Mapping[str, Any]): Eine Funktion, die die __annotate__ Funktion aus einem Klassennamen-Dictionary zurückgibt oder None, wenn keine vorhanden ist. Dies ist in Metaklassen während der Klassenerstellung nützlich. Es ist eine separate Funktion, um Implementierungsdetails über den internen Speicher für die __annotate__ Funktion zu vermeiden (siehe weiter unten).
  • Format: ein Enum, das die möglichen Formate von Annotationen enthält. Dies ersetzt die Formate VALUE, FORWARDREF und SOURCE in PEP 649. PEP 649 schlug vor, diese Werte als globale Mitglieder des Moduls inspect hinzuzufügen; wir ziehen es vor, sie innerhalb eines Enums zu platzieren. Wir schlagen die Hinzufügung eines vierten Formats vor, VALUE_WITH_FAKE_GLOBALS (siehe unten).
  • ForwardRef: eine Klasse, die eine Vorwärtsreferenz darstellt; sie kann von get_annotations() zurückgegeben werden, wenn das Format FORWARDREF ist. Die bestehende Klasse typing.ForwardRef wird zu einem Alias dieser Klasse. Ihre Mitglieder umfassen
    • __forward_arg__: das String-Argument der Vorwärtsreferenz
    • evaluate(globals=None, locals=None, type_params=None, owner=None): eine Methode, die versucht, die Vorwärtsreferenz auszuwerten. Das ForwardRef Objekt kann eine Referenz auf die Globals und andere Namensräume des Objekts halten, von dem es stammt. Wenn dies der Fall ist, können diese Namensräume zur Auswertung der Vorwärtsreferenz verwendet werden. Das Argument owner kann das Objekt sein, das die ursprüngliche Annotation enthält, z. B. das Klassen- oder Modulobjekt; es wird verwendet, um die Globals- und Locals-Namensräume zu extrahieren, wenn diese nicht angegeben sind.
    • _evaluate(), mit der gleichen Schnittstelle wie die bestehende Methode ForwardRef._evaluate. Sie wird undokumentiert und sofort veraltet sein. Sie wird zur Kompatibilität mit bestehenden Benutzern von typing.ForwardRef bereitgestellt.
  • call_annotate_function(func: Callable, format: Format): ein Helfer zum Aufrufen einer __annotate__ Funktion mit einem gegebenen Format. Wenn die Funktion dieses Format nicht unterstützt, richtet call_annotate_function() eine „Fake Globals“-Umgebung ein, wie in PEP 649 beschrieben, und verwendet diese Umgebung, um das gewünschte Annotationsformat zurückzugeben.
  • call_evaluate_function(func: Callable | None, format: Format): ähnlich wie call_annotate_function, aber ohne darauf angewiesen zu sein, dass die Funktion ein Annotations-Dictionary zurückgibt. Dies ist für die Auswertung von verzögerten Attributen gedacht, die durch PEP 695 und PEP 696 eingeführt wurden; siehe unten für Details. func kann der Einfachheit halber None sein; wenn None übergeben wird, gibt die Funktion ebenfalls None zurück.
  • annotations_to_string(annotations: dict[str, object]) -> dict[str, str]: eine Funktion, die jeden Wert in einem Annotations-Dictionary in eine String-Darstellung umwandelt. Dies ist nützlich, um das Format SOURCE in Fällen zu implementieren, in denen die ursprüngliche Quelle nicht verfügbar ist, z. B. in der Funktionssyntax für typing.TypedDict.
  • type_repr(value: object) -> str: eine Funktion, die einen einzelnen Wert in eine String-Darstellung umwandelt. Diese wird von annotations_to_string verwendet. Sie verwendet für die meisten Werte repr(), gibt aber für Typen den vollqualifizierten Namen zurück. Sie ist auch als Helfer für die repr() einer Reihe von Objekten in den Modulen typing und collections.abc nützlich.

Eine neue Funktion wird auch zum Modul typing hinzugefügt, typing.evaluate_forward_ref. Diese Funktion ist ein Wrapper um die Methode ForwardRef.evaluate, führt jedoch zusätzliche Arbeiten durch, die spezifisch für Typ-Hints sind. Sie rekursiert beispielsweise in komplexe Typen und wertet zusätzliche Vorwärtsreferenzen innerhalb dieser Typen aus.

Im Gegensatz zu PEP 649 werden die Annotationsformate (VALUE, FORWARDREF und SOURCE) nicht als globale Mitglieder des Moduls inspect hinzugefügt. Der einzig empfohlene Weg, auf diese Konstanten zu verweisen, ist annotationlib.Format.VALUE.

Abgelehnte Alternativen

Einen anderen Namen verwenden: Benennung ist schwierig, und ich habe mehrere Ideen in Betracht gezogen

  • annotations: Der offensichtlichste Name, aber er kann Verwirrung mit dem bestehenden from __future__ import annotations stiften, da Benutzer sowohl import annotations als auch from __future__ import annotations im selben Modul haben könnten. Die Verwendung eines gängigen Wortes als Namen erschwert die Suche nach dem Modul. Es gibt ein PyPI-Paket annotations, das jedoch nur eine einzige Veröffentlichung im Jahr 2015 hatte und verlassen aussieht.
  • annotation (im Singular): Ähnlich, aber verursacht keine Verwirrung mit dem Future-Import. Es gibt ein verlassenes PyPI-Paket annotation, das jedoch scheinbar nie Artefakte veröffentlicht hat.
  • annotools: Analog zu itertools und functools, aber "anno" ist eine weniger offensichtliche Abkürzung als "iter" oder "func". Zum Zeitpunkt des Verfassens gibt es kein PyPI-Paket mit diesem Namen.
  • annotationtools: Eine explizitere Version. Es gibt ein PyPI-Paket annotationtools, das 2023 eine Veröffentlichung hatte.
  • annotation_tools: Eine Variante des obigen, aber ohne PyPI-Konflikt. Jedoch hat kein anderes öffentliches Modul der Standardbibliothek einen Unterstrich im Namen.
  • annotationslib: Analog zu tomllib, pathlib und importlib. Es gibt kein PyPI-Paket mit diesem Namen.
  • annotationlib: Ähnlich wie oben, aber ein Zeichen kürzer und subjektiv besser lesbar. Ebenfalls nicht auf PyPI vergeben.

annotationlib scheint die beste Option zu sein.

Die Funktionalität zum inspect-Modul hinzufügen: Wie oben beschrieben, ist das Modul inspect bereits recht groß, und seine Importzeit ist für einige Anwendungsfälle unerschwinglich.

Die Funktionalität zum typing-Modul hinzufügen: Obwohl Annotationen hauptsächlich für Typ-Hints verwendet werden, können sie auch für andere Zwecke genutzt werden. Wir ziehen es vor, eine saubere Trennung zwischen Funktionalität zur Introspektion von Annotationen und Funktionalität, die ausschließlich für Typ-Hints bestimmt ist, beizubehalten.

Die Funktionalität zum types-Modul hinzufügen: Das Modul types ist für Funktionalität im Zusammenhang mit Typen gedacht, und Annotationen können auf Funktionen und Modulen existieren, nicht nur auf Typen.

Diese Funktionalität in einem Drittanbieterpaket entwickeln: Die Funktionalität in diesem neuen Modul wird reiner Python-Code sein, und es ist möglich, ein Drittanbieterpaket zu implementieren, das die gleiche Funktionalität durch direkte Interaktion mit den vom Interpreter generierten __annotate__-Funktionen bereitstellt. Die Funktionalität des vorgeschlagenen neuen Moduls wird jedoch sicherlich auch in der Standardbibliothek selbst nützlich sein (z. B. für die Implementierung von dataclasses und typing.NamedTuple), so dass es sinnvoll ist, sie in die Standardbibliothek aufzunehmen.

Diese Funktionalität zu einem privaten Modul hinzufügen: Es wäre möglich, das Modul zunächst in einem privaten Modul der Standardbibliothek (z. B. _annotations) zu entwickeln und es erst zu veröffentlichen, nachdem wir mehr Erfahrung mit der API gesammelt haben. Wir wissen jedoch bereits, dass wir Teile dieses Moduls für die Standardbibliothek selbst benötigen werden (z. B. für die Implementierung von dataclasses und typing.NamedTuple). Selbst wenn wir es privat machen, wird das Modul unweigerlich von Drittanbietern genutzt werden. Es ist vorzuziehen, von Anfang an mit einer klaren, dokumentierten API zu beginnen, um Drittanbietern die vollständige Unterstützung der PEP 649-Semantik zu ermöglichen, die auch die Standardbibliothek abdeckt. Das Modul wird sofort in anderen Teilen der Standardbibliothek verwendet werden, was sicherstellt, dass es eine angemessene Bandbreite von Anwendungsfällen abdeckt.

Verhalten der REPL

PEP 649 legt das folgende Verhalten der interaktiven REPL fest.

Der Einfachheit halber verzichten wir in diesem Fall auf die verzögerte Auswertung. Modulweite Annotationen in der REPL-Shell funktionieren weiterhin genau wie bei der „Standardsemantik“, sie werden sofort ausgewertet und das Ergebnis direkt im __annotations__-Dictionary gespeichert.

Dieses vorgeschlagene Verhalten birgt mehrere Probleme. Es macht die REPL zum einzigen Kontext, in dem Annotationen noch sofort ausgewertet werden, was für die Benutzer verwirrend ist und die Sprache verkompliziert.

Es erschwert auch die Implementierung der REPL, da sichergestellt werden muss, dass alle Anweisungen im „interaktiven“ Modus kompiliert werden, auch wenn ihre Ausgabe nicht angezeigt werden muss. (Dies ist relevant, wenn mehrere Anweisungen in einer einzigen Zeile von der REPL ausgewertet werden.)

Am wichtigsten ist, dass dies einige plausible Anwendungsfälle untergräbt, auf die unerfahrene Benutzer stoßen könnten. Ein Benutzer könnte Folgendes in eine Datei schreiben:

a: X | None = None
class X: ...

Unter PEP 649 würde dies einwandfrei funktionieren: X ist noch nicht definiert, wenn es in der Annotation für a verwendet wird, aber die Annotation wird verzögert ausgewertet. Wenn ein Benutzer jedoch denselben Code in die REPL kopiert und zeilenweise ausführt, würde dies einen NameError auslösen, da der Name X noch nicht definiert ist.

Dieses Thema wurde bereits auf Discourse diskutiert.

Spezifikation

Wir schlagen vor, die interaktive Konsole wie jeden anderen Modulcode zu behandeln und Annotationen verzögert auszuwerten. Dies macht die Sprache konsistenter und vermeidet subtile Verhaltensänderungen zwischen Modulen und der REPL.

Da die REPL zeilenweise ausgewertet wird, generieren wir eine neue __annotate__-Funktion für jede ausgewertete Anweisung im globalen Geltungsbereich, die Annotationen enthält. Jedes Mal, wenn eine Zeile mit Annotationen ausgewertet wird, geht die vorherige __annotate__-Funktion verloren.

>>> x: int
>>> __annotate__(1)
{'x': <class 'int'>}
>>> y: str
>>> __annotate__(1)
{'y': <class 'str'>}
>>> z: doesntexist
>>> __annotate__(1)
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
    __annotate__(1)
    ~~~~~~~~~~~~^^^
File "<python-input-4>", line 1, in __annotate__
    z: doesntexist
       ^^^^^^^^^^^
NameError: name 'doesntexist' is not defined

Es wird keinen __annotations__-Schlüssel im globalen Namensraum der REPL geben. In Modul-Namensräumen wird dieser Schlüssel verzögert erstellt, wenn auf den __annotations__-Deskriptor des Modulobjekts zugegriffen wird, aber in der REPL gibt es kein solches Modulobjekt.

Innerhalb der REPL definierte Klassen und Funktionen verhalten sich ebenfalls wie jede andere Klasse, so dass die Auswertung ihrer Annotationen verzögert wird. Es ist möglich, auf die Attribute __annotations__ und __annotate__ zuzugreifen oder das Modul annotationlib zur Introspektion der Annotationen zu verwenden.

Wrapper, die __annotations__ bereitstellen

Mehrere Objekte in der Standardbibliothek und anderswo stellen Annotationen für ihr umhülltes Objekt bereit. PEP 649 legt nicht fest, wie solche Wrapper sich verhalten sollen.

Spezifikation

Wrapper, die Annotationen bereitstellen, sollten mit den folgenden Zielen entworfen werden:

  • Die Auswertung von __annotations__ sollte so lange wie möglich verzögert werden, im Einklang mit dem Verhalten von integrierten Funktionen, Klassen und Modulen.
  • Die Abwärtskompatibilität mit dem Verhalten vor der Implementierung von PEP 649 sollte beibehalten werden.
  • Die Attribute __annotate__ und __annotations__ sollten beide mit einer Semantik bereitgestellt werden, die mit der des umhüllten Objekts übereinstimmt.

Genauer gesagt:

  • functools.update_wrapper() (und damit functools.wraps()) kopiert nur das __annotate__-Attribut vom umhüllten Objekt auf den Wrapper. Der __annotations__-Deskriptor der Wrapper-Funktion verwendet das kopierte __annotate__.
  • Die Konstruktoren für classmethod() und staticmethod() kopieren derzeit das __annotations__-Attribut vom umhüllten Objekt auf den Wrapper. Sie erhalten stattdessen schreibbare Attribute für __annotate__ und __annotations__. Das Lesen dieser Attribute ruft das entsprechende Attribut des zugrunde liegenden Aufrufbaren ab und speichert es im Cache im __dict__ des Wrappers. Das Schreiben auf diese Attribute aktualisiert direkt das __dict__, ohne den umhüllten Aufrufbaren zu beeinflussen.

Annotationen und Metaklassen

Tests der anfänglichen Implementierung dieses PEPs ergaben ernste Probleme bei der Interaktion zwischen Metaklassen und Klassenannotationen.

Vorhandene Fehler

Bei der Untersuchung der in diesem PEP zu spezifizierenden Verhaltensweisen haben wir mehrere Fehler im bestehenden Verhalten von __annotations__ in Klassen gefunden. Die Behebung dieser Fehler in Python 3.13 und früher liegt außerhalb des Rahmens dieses PEPs, aber sie werden hier aufgeführt, um die Eckfälle zu erklären, die behandelt werden müssen.

Zur Veranschaulichung: In Python 3.10 bis 3.13 wird das __annotations__-Dictionary im Klassen-Namensraum platziert, wenn die Klasse Annotationen hat. Wenn dies nicht der Fall ist, gibt es keinen __annotations__-Schlüssel im Klassen-Dictionary, wenn die Klasse erstellt wird, aber der Zugriff auf cls.__annotations__ ruft einen Deskriptor auf, der auf type definiert ist und ein leeres Dictionary zurückgibt und es im Klassen-Dictionary speichert. Statische Typen sind eine Ausnahme: sie haben nie Annotationen, und der Zugriff auf .__annotations__ löst einen AttributeError aus. In Python 3.9 und früher war das Verhalten anders; siehe gh-88067.

Der folgende Code schlägt identisch fehl in Python 3.10 bis 3.13.

class Meta(type): pass

class X(metaclass=Meta):
    a: str

class Y(X): pass

Meta.__annotations__  # important
assert Y.__annotations__ == {}, Y.__annotations__  # fails: {'a': <class 'str'>}

Wenn auf die Annotationen der Metaklasse Meta zugegriffen wird, bevor auf die Annotationen von Y zugegriffen wird, werden die Annotationen der Basisklasse X an Y geleakt. Wenn jedoch nicht auf die Annotationen der Metaklasse zugegriffen wird (d. h. die Zeile Meta.__annotations__ oben entfernt wird), sind die Annotationen für Y korrekt leer.

Ebenso lecken Annotationen aus annotierten Metaklassen zu nicht annotierten Klassen, die Instanzen der Metaklasse sind.

class Meta(type):
    a: str

class X(metaclass=Meta):
    pass

assert X.__annotations__ == {}, X.__annotations__  # fails: {'a': <class 'str'>}

Der Grund für dieses Verhalten ist, dass, wenn die Metaklasse einen __annotations__-Eintrag in ihrem Klassen-Dictionary enthält, dies verhindert, dass Instanzen der Metaklasse den __annotations__-Daten-Deskriptor der Basisklasse type verwenden. Im ersten Fall setzt der Zugriff auf Meta.__annotations__ Meta.__dict__["__annotations__"] = {} als Nebeneffekt. Wenn dann das Attribut __annotations__ von Y gesucht wird, sieht es zuerst das Metaklassen-Attribut, überspringt es aber, da es sich um einen Daten-Deskriptor handelt. Als Nächstes sucht es in den Klassen-Dictionaries der Klassen in seinem Method Resolution Order (MRO), findet X.__annotations__ und gibt es zurück. Im zweiten Beispiel gibt es keine Annotationen im MRO, sodass type.__getattribute__ auf die Rückgabe des Metaklassen-Attributs zurückfällt.

Metaklassenverhalten mit PEP 649

Mit PEP 649 wird das Verhalten des Zugriffs auf das Attribut .__annotations__ auf Klassen bei Beteiligung von Metaklassen noch inkonsistenter, da nun __annotations__ auch für Klassen mit Annotationen nur verzögert zum Klassen-Dictionary hinzugefügt wird. Das neue Attribut __annotate__ wird ebenfalls verzögert für Klassen ohne Annotationen erstellt, was zu weiteren Fehlverhalten bei Beteiligung von Metaklassen führt.

Ursache dieser Probleme ist, dass wir die Einträge __annotate__ und __annotations__ in den Klassen-Dictionaries nur unter bestimmten Umständen setzen und uns auf Deskriptoren verlassen, die auf type definiert sind, um sie bei Bedarf zu ergänzen. Bei normaler Attributsuche bricht dieser Ansatz in Gegenwart von Metaklassen zusammen, da Einträge im eigenen Klassen-Dictionary der Metaklasse die Deskriptoren unsichtbar machen können.

Wir haben mehrere Lösungen in Betracht gezogen und uns für eine entschieden, bei der wir die Objekte __annotate__ und __annotations__ im Klassen-Dictionary speichern, jedoch unter einem anderen, nur intern verwendeten Namen. Das bedeutet, dass die Einträge im Klassen-Dictionary nicht mit den Deskriptoren auf type interferieren werden.

Dieser Ansatz bedeutet, dass die Objekte .__annotate__ und .__annotations__ in Klassenobjekten weitgehend intuitiv funktionieren werden, es gibt jedoch einige Nachteile.

Einer betrifft die Interaktion mit Klassen, die unter from __future__ import annotations definiert sind. Diese werden weiterhin den __annotations__-Eintrag im Klassen-Dictionary haben, was bedeutet, dass sie weiterhin einige fehlerhafte Verhaltensweisen aufweisen. Wenn beispielsweise eine Metaklasse mit dem aktivierten __future__-Import definiert ist und Annotationen hat, und eine Klasse, die diese Metaklasse verwendet, ohne den __future__-Import definiert ist, liefert der Zugriff auf .__annotations__ auf dieser Klasse falsche Ergebnisse. Dieser Fehler besteht jedoch bereits in früheren Python-Versionen. Er könnte behoben werden, indem die Annotationen in diesem Fall ebenfalls unter einem anderen Schlüssel im Klassen-Dict gesetzt werden, aber das würde Benutzer brechen, die direkt auf das Klassen-Dictionary zugreifen (z. B. während der Klassenerstellung). Wir ziehen es vor, das Verhalten unter dem __future__-Import so weit wie möglich unverändert zu lassen.

Zweitens war es in früheren Python-Versionen möglich, auf das Attribut __annotations__ von Instanzen benutzerdefinierter Klassen mit Annotationen zuzugreifen. Dieses Verhalten war jedoch undokumentiert und wurde von inspect.get_annotations() nicht unterstützt und kann im PEP 649-Framework nicht ohne größere Änderungen, wie z. B. einen neuen object.__annotations__-Deskriptor, beibehalten werden. Diese Verhaltensänderung sollte in Portierungsleitfäden erwähnt werden.

Spezifikation

Die Attribute .__annotate__ und .__annotations__ auf Klassenobjekten sollten zuverlässig die Annotationsfunktion bzw. das Annotations-Dictionary zurückgeben, auch in Gegenwart von benutzerdefinierten Metaklassen.

Benutzer sollten nicht direkt auf das Klassen-Dictionary zugreifen, um Annotationen oder die Annotationsfunktion abzurufen; die im Klassen-Dictionary gespeicherten Daten sind ein Implementierungsdetail und ihr Format kann sich in Zukunft ändern. Wenn nur das Klassen-Namensraum-Dictionary verfügbar ist (z. B. während der Klassenerstellung), kann annotationlib.get_annotate_from_class_namespace verwendet werden, um die Annotationsfunktion aus dem Klassen-Dictionary abzurufen.

Abgelehnte Alternativen

Wir haben drei allgemeine Ansätze für den Umgang mit dem Verhalten der Einträge __annotations__ und __annotate__ in Klassen in Betracht gezogen:

  • Sicherstellen, dass der Eintrag *immer* im Klassen-Dictionary vorhanden ist, auch wenn er leer ist oder noch nicht ausgewertet wurde. Das bedeutet, wir müssen uns nicht auf die Deskriptoren verlassen, die auf type definiert sind, um das Feld zu füllen, und daher werden die Attribute der Metaklasse nicht stören. (Prototyp in gh-120719.)
  • Benutzer warnen, die Attribute __annotations__ und __annotate__ direkt zu verwenden. Stattdessen sollten Benutzer Funktionen in annotationlib aufrufen, die die type-Deskriptoren direkt aufrufen. (Implementiert in gh-122074.)
  • Sicherstellen, dass der Eintrag *niemals* im Klassen-Dictionary vorhanden ist, oder zumindest nie durch Logik im Sprachkern hinzugefügt wird. Das bedeutet, dass die Deskriptoren auf type immer verwendet werden, ohne Störung durch die Metaklasse. (Erster Prototyp in gh-120816; später implementiert in gh-132345.)

Alex Waygood schlug eine Implementierung nach dem ersten Ansatz vor. Wenn ein Heap-Typ (wie eine Klasse, die über die class-Anweisung erstellt wird) erstellt wird, wird cls.__dict__["__annotations__"] auf einen speziellen Deskriptor gesetzt. Bei __get__ wertet der Deskriptor die Annotationen aus, indem er __annotate__ aufruft und das Ergebnis zurückgibt. Das Annotations-Dictionary wird innerhalb der Deskriptor-Instanz im Cache gespeichert. Der Deskriptor verhält sich auch wie eine Zuordnung, so dass Code, der cls.__dict__["__annotations__"] verwendet, normalerweise weiterhin funktioniert: Wenn das Objekt als Zuordnung behandelt wird, werden die Annotationen ausgewertet und das Verhalten ist so, als ob der Deskriptor selbst das Annotations-Dictionary wäre. (Code, der davon ausgeht, dass cls.__dict__["__annotations__"] spezifisch eine Instanz von dict ist, könnte jedoch fehlschlagen.)

Dieser Ansatz ist auch für __annotate__ einfach zu implementieren: Dieses Attribut ist für Klassen mit Annotationen bereits immer gesetzt, und wir können es explizit auf None für Klassen ohne Annotationen setzen.

Während dieser Ansatz die bekannten Ausnahmefälle mit Metaklassen beheben würde, führt er zu erheblicher Komplexität für alle Klassen, einschließlich eines neuen integrierten Typs (für den Annotations-Deskriptor) mit ungewöhnlichem Verhalten.

Der zweite Ansatz ist einfach zu implementieren, hat aber den Nachteil, dass der direkte Zugriff auf cls.__annotations__ weiterhin anfällig für inkonsistentes Verhalten ist.

Hinzufügen des VALUE_WITH_FAKE_GLOBALS Formats

PEP 649 legt fest.

Dieses PEP geht davon aus, dass Drittanbieterbibliotheken ihre eigenen __annotate__-Methoden implementieren können und dass diese Funktionen bei Ausführung in dieser „Fake Globals“-Umgebung fast sicher falsch funktionieren würden. Aus diesem Grund weist dieses PEP eine Flagge für Code-Objekte zu, eines der ungenutzten Bits in co_flags, das bedeutet „Dieses Code-Objekt kann in einer ‚Fake Globals‘-Umgebung ausgeführt werden.“ Dies macht die „Fake Globals“-Umgebung strikt opt-in, und es wird erwartet, dass nur von __annotate__-Methoden, die vom Python-Compiler generiert werden, gesetzt wird.

Dieser Mechanismus koppelt die Implementierung jedoch mit Low-Level-Details des Code-Objekts. Die Code-Objekt-Flags sind CPython-spezifisch und die Dokumentation warnt ausdrücklich davor, sich auf die Werte zu verlassen.

Larry Hastings schlug einen alternativen Ansatz vor, der nicht auf Code-Flags basiert: ein viertes Format, VALUE_WITH_FAKE_GLOBALS. Vom Compiler generierte Annotate-Funktionen würden nur die Formate VALUE und VALUE_WITH_FAKE_GLOBALS unterstützen, die beide identisch implementiert sind. Die Standardbibliothek würde das Format VALUE_WITH_FAKE_GLOBALS verwenden, wenn sie eine Annotate-Funktion in einer der speziellen „Fake Globals“-Umgebungen aufruft.

Dieser Ansatz ist als zukunftssicherer Mechanismus für die Hinzufügung neuer Annotationsformate in der Zukunft nützlich. Benutzer, die Annotate-Funktionen manuell schreiben, sollten NotImplementedError auslösen, wenn das Format VALUE_WITH_FAKE_GLOBALS angefordert wird, damit die Standardbibliothek die manuell geschriebene Annotate-Funktion nicht mit „Fake Globals“ aufruft, was unvorhersehbare Ergebnisse haben könnte.

Die Namen der Annotationsformate geben an, welche Art von Objekten eine __annotate__-Funktion zurückgeben sollte: mit dem Format STRING sollte sie Strings zurückgeben; mit dem Format FORWARDREF sollte sie Forward References zurückgeben; und mit dem Format VALUE sollte sie Werte zurückgeben. Der Name VALUE_WITH_FAKE_GLOBALS gibt an, dass die Funktion immer noch Werte zurückgeben soll, aber in einer ungewöhnlichen „Fake Globals“-Umgebung ausgeführt wird.

Spezifikation

Ein zusätzliches Format, VALUE_WITH_FAKE_GLOBALS, wird zur Format-Enum im Modul annotationlib hinzugefügt, mit dem Wert 2. (Dadurch verschieben sich die Werte der anderen Formate im Vergleich zu PEP 649: FORWARDREF wird 3 und SOURCE wird 4 sein.) Die ganzzahligen Werte dieser Formate sind für Stellen spezifiziert, an denen die Enum nicht ohne weiteres verfügbar ist, z. B. in __annotate__-Funktionen, die in C implementiert sind.

Vom Compiler generierte Annotate-Funktionen unterstützen dieses Format und geben denselben Wert zurück, den sie für das Format VALUE zurückgeben würden. Die Standardbibliothek übergibt dieses Format an die __annotate__-Funktion, wenn sie in einer „Fake Globals“-Umgebung aufgerufen wird, wie sie zur Implementierung der Formate FORWARDREF und SOURCE verwendet wird. Alle öffentlichen Funktionen im Modul annotationlib, die ein Format-Argument akzeptieren, lösen NotImplementedError aus, wenn das Format VALUE_WITH_FAKE_GLOBALS ist.

Drittanbieter-Code, der __annotate__-Funktionen implementiert, sollte NotImplementedError auslösen, wenn das Format VALUE_WITH_FAKE_GLOBALS übergeben wird und die Funktion nicht darauf vorbereitet ist, in einer „Fake Globals“-Umgebung ausgeführt zu werden. Dies sollte in der Datenmodell-Dokumentation für __annotate__ erwähnt werden.

Auswirkung des Löschens von __annotations__

PEP 649 legt fest.

Das Setzen von o.__annotations__ auf einen gültigen Wert setzt automatisch o.__annotate__ auf None.

Das PEP besagt jedoch nicht, was passiert, wenn das Attribut __annotations__ gelöscht wird (mit del). Es scheint am konsistentesten, dass das Löschen des Attributs auch __annotate__ löscht.

Spezifikation

Das Löschen des Attributs __annotations__ auf Funktionen, Modulen und Klassen führt dazu, dass __annotate__ auf None gesetzt wird.

Verzögerte Auswertung von PEP 695 und 696 Objekten

Seit PEP 649 geschrieben wurde, haben Python 3.12 und 3.13 Unterstützung für mehrere neue Funktionen erhalten, die ebenfalls eine verzögerte Auswertung verwenden, ähnlich dem Verhalten, das dieses PEP für Annotationen vorschlägt.

Derzeit verwenden diese Objekte eine verzögerte Auswertung, aber es gibt keinen direkten Zugriff auf das Funktionsobjekt, das für die verzögerte Auswertung verwendet wird. Um die gleiche Art von Introspektion zu ermöglichen, die jetzt für Annotationen möglich ist, schlagen wir vor, die internen Funktions-Objekte freizugeben und es Benutzern zu ermöglichen, sie mit den Formaten FORWARDREF und SOURCE auszuwerten.

Spezifikation

Wir werden die folgenden neuen Attribute hinzufügen:

Mit Ausnahme von evaluate_value können diese Attribute None sein, wenn das Objekt keine Bindung, Einschränkungen oder einen Standardwert hat. Andernfalls ist das Attribut ein aufrufbares Objekt, ähnlich einer __annotate__-Funktion, das ein einzelnes ganzzahliges Argument annimmt und den ausgewerteten Wert zurückgibt. Im Gegensatz zu __annotate__-Funktionen geben diese aufrufbaren Objekte einen einzelnen Wert zurück, nicht ein Dictionary von Annotationen. Diese Attribute sind schreibgeschützt.

Normalerweise würden Benutzer diese Attribute in Kombination mit annotationlib.call_evaluate_function verwenden. Um beispielsweise die Bindung eines TypeVar im SOURCE-Format zu erhalten, könnte man schreiben annotationlib.call_evaluate_function(T.evaluate_bound, annotationlib.Format.SOURCE).

Verhalten von Dataclass-Feldtypen

Eine Folge der verzögerten Auswertung von Annotationen ist, dass Dataclasses Forward References in ihren Annotationen verwenden können.

>>> from dataclasses import dataclass
>>> @dataclass
... class D:
...     x: undefined
...

Das FORWARDREF-Format sickert jedoch in die Feldtypen des Dataclasses ein.

>>> fields(D)[0].type
ForwardRef('undefined')

Wir haben eine Änderung in Betracht gezogen, bei der das Attribut .type eines Feldobjekts die Auswertung von Annotationen auslösen würde, so dass der Feldtyp tatsächliche Werte enthalten könnte, falls Forward References nach der Erstellung des Dataclasses definiert wurden, bevor auf den Feldtyp zugegriffen wurde. Dies würde jedoch auch bedeuten, dass der Zugriff auf .type nun beliebigen Code in der Annotation ausführen und potenziell Fehler wie NameError auslösen könnte.

Daher halten wir es für benutzerfreundlicher, das ForwardRef-Objekt im Typ zu belassen und zu dokumentieren, dass Benutzer, die Forward References auflösen möchten, die Methode ForwardRef.evaluate verwenden können.

Wenn in Zukunft Anwendungsfälle auftreten, könnten wir zusätzliche Funktionalität hinzufügen, wie z. B. eine neue Methode, die die Annotation von Grund auf neu auswertet.

Umbenennung von SOURCE zu STRING

Das Format SOURCE ist für Werkzeuge gedacht, die ein für Menschen lesbares Format anzeigen müssen, das dem ursprünglichen Quellcode nahekommt. Wir können jedoch den ursprünglichen Quellcode in __annotate__-Funktionen nicht abrufen, und in einigen Fällen haben wir __annotate__-Funktionen in Python-Code, der keinen Zugriff auf den ursprünglichen Code hat. Dies gilt zum Beispiel für dataclasses.make_dataclass() und die aufrufbasierte Syntax für typing.TypedDict.

Dies macht den Namen SOURCE etwas irreführend. Das Ziel des Formats sollte in der Tat darin bestehen, die Quelle wiederherzustellen, aber der Name wird Benutzer in der Praxis wahrscheinlich in die Irre führen. Ein neutralerer Name würde betonen, dass das Format ein Anmerkungs-Dictionary nur mit Strings zurückgibt. Wir schlagen STRING vor.

Spezifikation

Das Format SOURCE wird in STRING umbenannt. Um die Änderungen in diesem PEP zu wiederholen, sind die vier unterstützten Formate nun:

  • VALUE: das Standardformat, das die Anmerkungen auswertet und die resultierenden Werte zurückgibt.
  • VALUE_WITH_FAKE_GLOBALS: zur internen Verwendung; sollte wie VALUE von Annotate-Funktionen behandelt werden, die die Ausführung mit gefälschten Globals unterstützen.
  • FORWARDREF: ersetzt undefinierte Namen durch ForwardRef-Objekte.
  • STRING: gibt Strings zurück, versucht, Code nahe am ursprünglichen Quellcode wiederherzustellen.

Bedingt definierte Annotationen

PEP 649 unterstützt keine Anmerkungen, die bedingt im Körper einer Klasse oder eines Moduls definiert sind.

Es ist derzeit möglich, Modul- und Klassenattribute mit Anmerkungen innerhalb einer if- oder try-Anweisung zu setzen, und es funktioniert wie erwartet. Es ist unhaltbar, dieses Verhalten zu unterstützen, wenn dieser PEP aktiv ist.

Der Wartungsbeauftragte der weit verbreiteten SQLAlchemy-Bibliothek berichtete jedoch, dass dieses Muster tatsächlich üblich und wichtig ist.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from some_module import SpecialType

class MyClass:
    somevalue: str
    if TYPE_CHECKING:
        someothervalue: SpecialType

Unter dem in PEP 649 vorgesehenen Verhalten würde __annotations__ für MyClass Schlüssel sowohl für somevalue als auch für someothervalue enthalten.

Glücklicherweise gibt es eine beherrschbare Implementierungsstrategie, um diesen Code wieder wie erwartet funktionieren zu lassen. Diese Strategie beruht auf einigen glücklichen Umständen.

  • Diese Verhaltensänderung ist nur für Modul- und Klassenanmerkungen relevant, da Anmerkungen in lokalen Geltungsbereichen ignoriert werden.
  • Modul- und Körper von Klassen werden nur einmal ausgeführt.
  • Die Anmerkungen einer Klasse sind erst nach Abschluss der Ausführung des Klassenkörpers extern sichtbar. Bei Modulen ist dies nicht ganz richtig, da ein teilweise ausgeführtes Modul für andere importierte Module sichtbar sein kann, aber dieser Fall ist aus anderen Gründen problematisch (siehe nächster Abschnitt).

Dies ermöglicht die folgende Implementierungsstrategie:

  • Jeder Anmerkungszuweisung wird eine eindeutige Kennung zugewiesen (z. B. eine Ganzzahl).
  • Während der Ausführung eines Klassen- oder Modulkörpers wird ein zunächst leeres Set erstellt, um die Kennungen der definierten Anmerkungen zu speichern.
  • Wenn eine Anmerkungszuweisung ausgeführt wird, wird ihre Kennung zum Set hinzugefügt.
  • Die generierte __annotate__-Funktion verwendet das Set, um zu bestimmen, welche Anmerkungen im Klassen- oder Modulkörper definiert wurden, und gibt nur diese zurück.

Dies wurde in python/cpython#130935 implementiert.

Spezifikation

Für Klassen und Module gibt die __annotate__-Funktion nur Anmerkungen für die Zuweisungen zurück, die bei der Ausführung des Klassen- oder Modulkörpers ausgeführt wurden.

Caching von Annotationen auf teilweise ausgeführten Modulen

PEP 649 gibt an, dass der Wert des Attributs __annotations__ auf Klassen und Modulen bei der ersten Abfrage durch Aufruf der Funktion __annotate__ bestimmt wird und dann für spätere Abfragen zwischengespeichert wird. Dies ist in den meisten Fällen korrekt und erhält die Kompatibilität, aber es gibt einen Grenzfall, in dem es zu überraschendem Verhalten führen kann: teilweise ausgeführte Module.

Betrachten Sie dieses Beispiel:

# recmod/__main__.py
from . import a
print("in __main__:", a.__annotations__)

# recmod/a.py
v1: int
from . import b
v2: int

# recmod/b.py
from . import a
print("in b:", a.__annotations__)

Beachten Sie, dass während recmod/b.py ausgeführt wird, das Modul recmod.a definiert ist, aber noch nicht vollständig ausgeführt wurde.

Auf 3.13 ergibt dies:

$ python3.13 -m recmod
in b: {'v1': <class 'int'>}
in __main__: {'v1': <class 'int'>, 'v2': <class 'int'>}

Aber mit PEP 649, wie ursprünglich vorgeschlagen, würde dies zweimal ein leeres Dictionary ausgeben, da die __annotate__-Funktion erst gesetzt wird, wenn die Modulausführung abgeschlossen ist. Das ist offensichtlich unintuitiv.

Siehe python/cpython#131550 für die Implementierung.

Spezifikation

Der Zugriff auf __annotations__ bei einem teilweise ausgeführten Modul gibt weiterhin die bisher ausgeführten Anmerkungen zurück, ähnlich wie in früheren Python-Versionen. In diesem Fall wird das Dictionary __annotations__ jedoch nicht zwischengespeichert, sodass spätere Zugriffe auf das Attribut __annotations__ ein frisches Dictionary zurückgeben. Dies ist notwendig, da __annotate__ erneut aufgerufen werden muss, um zusätzliche Anmerkungen einzubeziehen.

Verschiedene Implementierungsdetails

PEP 649 geht detailliert auf einige Aspekte der Implementierung ein. Um Verwechslungen zu vermeiden, beschreiben wir einige Aspekte, bei denen sich die aktuelle Implementierung von der in dem PEP beschriebenen unterscheidet. Diese Details sind jedoch nicht garantiert und können sich zukünftig ohne Vorankündigung ändern, sofern sie nicht in der Sprachreferenz dokumentiert sind.

Unterstützte Operationen auf ForwardRef Objekten

Das Format SOURCE wird durch die „Stringizer“-Technik implementiert, bei der das Globals-Dictionary einer Funktion so erweitert wird, dass jeder Zugriff zu einem speziellen Objekt führt, das zur Rekonstruktion der Operationen verwendet werden kann, die auf dem Objekt ausgeführt werden.

PEP 649 legt fest.

In der Praxis wird die „Stringizer“-Funktionalität im ForwardRef-Objekt implementiert, das derzeit im typing-Modul definiert ist. ForwardRef wird erweitert, um die gesamte Stringizer-Funktionalität zu implementieren; es wird auch erweitert, um die Zeichenkette, die es enthält, auszuwerten, um den tatsächlichen Wert zu erzeugen (vorausgesetzt, alle referenzierten Symbole sind definiert).

Dies wird jedoch wahrscheinlich zu Verwirrung in der Praxis führen. Ein Objekt, das die Stringizer-Funktionalität implementiert, muss fast alle speziellen Methoden implementieren, einschließlich __getattr__ und __eq__, um einen neuen Stringizer zurückzugeben. Ein solches Objekt ist verwirrend zu handhaben: Alle Operationen gelingen, aber sie geben wahrscheinlich andere Objekte zurück als erwartet.

Die aktuelle Implementierung implementiert stattdessen nur wenige nützliche Methoden in der Klasse ForwardRef. Während der Auswertung von Anmerkungen wird anstelle von ForwardRef eine Instanz einer privaten Stringizer-Klasse verwendet. Nach Abschluss der Auswertung konvertiert die Implementierung des FORWARDREF-Formats diese internen Objekte in ForwardRef-Objekte.

Signatur von __annotate__ Funktionen

PEP 649 gibt die Signatur von __annotate__-Funktionen als

__annotate__(format: int) -> dict

Die Verwendung von format als Parameternamen könnte jedoch zu Kollisionen führen, wenn eine Anmerkung ein Symbol mit dem Namen format verwendet. Um dieses Problem zu vermeiden, verwendet die aktuelle Implementierung einen rein positionellen Parameter mit dem Namen format in der Funktionssignatur, der jedoch die Verwendung des Namens format innerhalb der Anmerkung nicht überschattet.

Abwärtskompatibilität

PEP 649 bietet eine gründliche Diskussion der Kompatibilitätsimplikationen für bestehenden Code, der entweder Standard- oder PEP 563-Semantik verwendet.

Es gibt jedoch eine weitere Reihe von Kompatibilitätsproblemen: neuer Code, der unter der Annahme von PEP 649-Semantik geschrieben wurde, aber bestehende Werkzeuge verwendet, die Anmerkungen vorzeitig auswerten. Betrachten Sie zum Beispiel einen dataclass-ähnlichen Klassen-Decorator @annotator, der die mit Anmerkungen versehenen Felder in der von ihm dekorierten Klasse abruft, entweder durch direkten Zugriff auf __annotations__ oder durch Aufruf von inspect.get_annotations().

Sobald PEP 649 implementiert ist, funktioniert Code wie dieser:

class X:
    y: Y

class Y: pass

Aber das wird nicht funktionieren, es sei denn, @annotator wird geändert, um das neue FORWARDREF-Format zu verwenden.

@annotator
class X:
    y: Y

class Y: pass

Dies ist keine strikte Abwärtskompatibilitätsproblematik, da kein zuvor funktionierender Code kaputtgehen würde; vor PEP 649 hätte dieser Code zur Laufzeit einen NameError ausgelöst. In gewissem Sinne unterscheidet es sich nicht von anderen neuen Python-Funktionen, die von Drittanbieterbibliotheken unterstützt werden müssen. Dennoch ist es ein ernstes Problem für Bibliotheken, die Introspektion betreiben, und es ist wichtig, dass wir es Bibliotheken so einfach wie möglich machen, die neuen Semantiken auf eine unkomplizierte und benutzerfreundliche Weise zu unterstützen.

Mehrere Funktionalitäten in der Standardbibliothek sind von diesem Problem betroffen, darunter dataclasses, typing.TypedDict und typing.NamedTuple. Diese wurden aktualisiert, um dieses Muster mithilfe der Funktionalität des neuen Moduls annotationlib zu unterstützen.

Sicherheitsimplikationen

Eine Konsequenz von PEP 649 ist, dass der Zugriff auf Anmerkungen eines Objekts, auch wenn das Objekt eine Funktion oder ein Modul ist, nun beliebigen Code ausführen kann. Dies gilt auch, wenn das STRING-Format verwendet wird, da der Stringizer-Mechanismus nur den globalen Namensraum überschreibt, was nicht ausreicht, um Python-Code vollständig zu sandkasten.

In früheren Python-Versionen konnte der Zugriff auf Anmerkungen von Funktionen oder Modulen keinen beliebigen Code ausführen, aber Klassen und andere Objekte konnten bereits beliebigen Code beim Zugriff auf das Attribut __annotations__ ausführen. Ebenso konnte fast jede weitere Introspektion der Anmerkungen (z. B. mit isinstance(), Aufruf von Funktionen wie typing.get_origin oder sogar Anzeige der Anmerkungen mit repr()) bereits beliebigen Code ausführen. Und natürlich impliziert der Zugriff auf Anmerkungen aus nicht vertrauenswürdigem Code, dass der nicht vertrauenswürdige Code bereits importiert wurde.

Wie man das lehrt

Die Semantik von PEP 649, wie sie durch diesen PEP modifiziert wurde, sollte für Benutzer, die Anmerkungen zu ihrem Code hinzufügen, weitgehend intuitiv sein. Wir beseitigen die Notwendigkeit, manuell Anführungszeichen um Anmerkungen zu setzen, die Vorwärtsreferenzen benötigen, was eine Hauptquelle der Verwirrung für Benutzer darstellt.

Für fortgeschrittene Benutzer, die Anmerkungen introspektieren müssen, wird die Sache komplexer. Die Dokumentation des neuen Moduls annotationlib dient als Referenz für Benutzer, die programmatisch mit Anmerkungen interagieren müssen.

Referenzimplementierung

Die in diesem PEP vorgeschlagenen Änderungen wurden auf dem Hauptzweig des CPython-Repositorys implementiert.

Danksagungen

Zunächst danke ich Larry Hastings für das Schreiben von PEP 649. Dieser PEP modifiziert einige seiner anfänglichen Entscheidungen, aber das Gesamtdesign ist immer noch sein.

Ich danke Carl Meyer und Alex Waygood für ihr Feedback zu frühen Entwürfen dieses PEP. Alex Waygood, Alyssa Coghlan und David Ellis gaben aufschlussreiches Feedback und Vorschläge zur Interaktion zwischen Metaklassen und __annotations__. Larry Hastings gab ebenfalls nützliches Feedback zu diesem PEP. Nikita Sobolev nahm verschiedene Änderungen an der Standardbibliothek vor, die die Funktionalität von PEP 649 nutzen, und seine Erfahrung trug zur Verbesserung des Designs bei.

Anhang

Welche Ausdrücke können als String dargestellt werden?

PEP 649 erkennt an, dass der Stringizer nicht alle Ausdrücke verarbeiten kann. Jetzt, da wir eine Entwurfsversion haben, können wir präziser darüber sein, welche Ausdrücke verarbeitet werden können und welche nicht. Nachfolgend finden Sie eine Liste aller Ausdrücke im Python AST, die vom Stringizer wiederhergestellt werden können und nicht können. Die vollständige Liste sollte wahrscheinlich nicht in die Dokumentation aufgenommen werden, aber ihre Erstellung ist eine nützliche Übung.

Erstens kann der Stringizer natürlich keine Informationen wiederherstellen, die nicht im kompilierten Code vorhanden sind, einschließlich Kommentaren, Leerzeichen, Klammerungen und Operationen, die vom AST-Optimierer vereinfacht werden.

Zweitens kann der Stringizer fast alle Operationen abfangen, die Namen betreffen, die in einem bestimmten Geltungsbereich nachgeschlagen werden, aber er kann keine Operationen abfangen, die vollständig auf Konstanten operieren. Als Korollar bedeutet dies auch, dass es nicht sicher ist, das Format SOURCE für nicht vertrauenswürdigen Code anzufordern: Python ist mächtig genug, dass es möglich ist, beliebige Codeausführung zu erreichen, selbst ohne Zugriff auf Globals oder Builtins. Zum Beispiel:

>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
...
>>> annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
Hello world
{'x': 'None'}

(Dieses spezielle Beispiel funktionierte für mich mit der aktuellen Implementierung eines Entwurfs dieses PEP; der genaue Code funktioniert möglicherweise in Zukunft nicht mehr.)

Die folgenden sind unterstützt (manchmal mit Einschränkungen):

  • BinOp
  • UnaryOp
    • Invert (~), UAdd (+) und USub (-) werden unterstützt.
    • Not (not) wird nicht unterstützt.
  • Dict (außer bei **-Unpacking)
  • Set
  • Compare
    • Eq und NotEq werden unterstützt.
    • Lt, LtE, Gt und GtE werden unterstützt, aber der Operand kann umgedreht werden.
    • Is, IsNot, In und NotIn werden nicht unterstützt.
  • Call (außer bei **-Unpacking)
  • Constant (jedoch nicht die exakte Darstellung der Konstante; zum Beispiel gehen Escape-Sequenzen in Strings verloren; Hexadezimalzahlen werden in Dezimalzahlen umgewandelt).
  • Attribute (vorausgesetzt, der Wert ist keine Konstante).
  • Subscript (vorausgesetzt, der Wert ist keine Konstante).
  • Starred (*-Unpacking).
  • Name
  • List
  • Tuple
  • Slice

Die folgenden sind nicht unterstützt, werfen aber beim Auftreten durch den Stringizer eine aussagekräftige Fehlermeldung:

  • FormattedValue (f-Strings; Fehler wird nicht erkannt, wenn Konvertierungsspezifizierer wie !r verwendet werden).
  • JoinedStr (f-Strings).

Die folgenden sind nicht unterstützt und führen zu falschen Ausgaben:

  • BoolOp (and und or).
  • IfExp
  • Lambda
  • ListComp
  • SetComp
  • DictComp
  • GeneratorExp

Die folgenden sind in Annotations-Geltungsbereichen nicht zulässig und daher nicht relevant:

  • NamedExpr (:=).
  • Await
  • Yield
  • YieldFrom

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

Zuletzt geändert: 2025-10-06 14:23:25 GMT