Annotationen Best Practices

Autor:

Larry Hastings

Zugriff auf das Annotations-Dictionary eines Objekts in Python 3.10 und neuer

Python 3.10 fügt der Standardbibliothek eine neue Funktion hinzu: inspect.get_annotations(). In den Python-Versionen 3.10 bis 3.13 ist das Aufrufen dieser Funktion die Best Practice für den Zugriff auf das Annotations-Dictionary jedes Objekts, das Annotationen unterstützt. Diese Funktion kann auch stringifizierte Annotationen für Sie "ent-stringifizieren".

In Python 3.14 gibt es ein neues annotationlib Modul mit Funktionalität zur Arbeit mit Annotationen. Dies beinhaltet eine Funktion annotationlib.get_annotations(), die inspect.get_annotations() ersetzt.

Wenn inspect.get_annotations() aus irgendeinem Grund für Ihren Anwendungsfall nicht praktikabel ist, können Sie das Datenmember __annotations__ manuell zugreifen. Die Best Practice hierfür hat sich ebenfalls in Python 3.10 geändert: Ab Python 3.10 ist garantiert, dass o.__annotations__ *immer* auf Python-Funktionen, Klassen und Module funktioniert. Wenn Sie sicher sind, dass das zu untersuchende Objekt eines dieser drei *spezifischen* Objekte ist, können Sie einfach o.__annotations__ verwenden, um auf das Annotations-Dictionary des Objekts zuzugreifen.

Andere Arten von Aufrufbaren – zum Beispiel Aufrufbare, die von functools.partial() erstellt wurden – haben möglicherweise kein __annotations__ Attribut definiert. Beim Zugriff auf __annotations__ eines möglicherweise unbekannten Objekts ist die Best Practice in Python-Versionen 3.10 und neuer, getattr() mit drei Argumenten aufzurufen, z. B. getattr(o, '__annotations__', None).

Vor Python 3.10 würde der Zugriff auf __annotations__ auf einer Klasse, die keine Annotationen definiert, aber eine Elternklasse mit Annotationen hat, die __annotations__ der Elternklasse zurückgeben. In Python 3.10 und neuer wird die Annotation der Kindklasse stattdessen ein leeres Dictionary sein.

Zugriff auf das Annotations-Dictionary eines Objekts in Python 3.9 und älter

In Python 3.9 und älter ist der Zugriff auf das Annotations-Dictionary eines Objekts wesentlich komplizierter als in neueren Versionen. Das Problem ist ein Designfehler in diesen älteren Python-Versionen, insbesondere im Hinblick auf Klassenannotationen.

Die Best Practice für den Zugriff auf das Annotations-Dictionary anderer Objekte – Funktionen, andere Aufrufbare und Module – ist die gleiche wie die Best Practice für 3.10, vorausgesetzt, Sie rufen nicht inspect.get_annotations() auf: Sie sollten ein dreistufiges getattr() verwenden, um auf das __annotations__ Attribut des Objekts zuzugreifen.

Leider ist dies keine Best Practice für Klassen. Das Problem ist, dass, da __annotations__ bei Klassen optional ist und da Klassen Attribute von ihren Basisklassen erben können, der Zugriff auf das __annotations__ Attribut einer Klasse versehentlich das Annotations-Dictionary einer *Basisklasse* zurückgeben kann. Als Beispiel

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

Dies gibt das Annotations-Dictionary von Base aus, nicht von Derived.

Ihr Code muss einen separaten Pfad haben, wenn das zu untersuchende Objekt eine Klasse ist (isinstance(o, type)). In diesem Fall basiert die Best Practice auf einem Implementierungsdetail von Python 3.9 und früher: Wenn eine Klasse Annotationen definiert hat, werden diese im __dict__ Dictionary der Klasse gespeichert. Da die Klasse möglicherweise Annotationen definiert hat oder nicht, besteht die Best Practice darin, die get() Methode des Klassen-Dictionaries aufzurufen.

Um alles zusammenzufassen, hier ist ein Beispielcode, der sicher auf das __annotations__ Attribut eines beliebigen Objekts in Python 3.9 und früher zugreift

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

Nachdem dieser Code ausgeführt wurde, sollte ann entweder ein Dictionary oder None sein. Sie werden ermutigt, den Typ von ann mit isinstance() zu überprüfen, bevor Sie ihn weiter untersuchen.

Beachten Sie, dass einige exotische oder fehlerhafte Typobjekte möglicherweise kein __dict__ Attribut haben, daher können Sie für zusätzliche Sicherheit auch getattr() verwenden, um auf __dict__ zuzugreifen.

Manuelles Ent-Stringifizieren von stringifizierten Annotationen

In Situationen, in denen einige Annotationen "stringifiziert" sein mögen und Sie diese Strings auswerten möchten, um die Python-Werte zu erzeugen, die sie darstellen, ist es am besten, inspect.get_annotations() aufzurufen, um diese Arbeit für Sie zu erledigen.

Wenn Sie Python 3.9 oder älter verwenden oder wenn Sie inspect.get_annotations() aus irgendeinem Grund nicht verwenden können, müssen Sie seine Logik duplizieren. Sie werden ermutigt, die Implementierung von inspect.get_annotations() in der aktuellen Python-Version zu untersuchen und einen ähnlichen Ansatz zu verfolgen.

Kurz gesagt, wenn Sie eine stringifizierte Annotation auf einem beliebigen Objekt o auswerten möchten

  • Wenn o ein Modul ist, verwenden Sie o.__dict__ als globals beim Aufruf von eval().

  • Wenn o eine Klasse ist, verwenden Sie sys.modules[o.__module__].__dict__ als globals und dict(vars(o)) als locals, beim Aufruf von eval().

  • Wenn o ein mit functools.update_wrapper(), functools.wraps() oder functools.partial() gewickeltes Aufrufbares ist, entpacken Sie es iterativ, indem Sie entweder o.__wrapped__ oder o.func wie zutreffend zugreifen, bis Sie die ursprüngliche entpackte Funktion gefunden haben.

  • Wenn o aufrufbar ist (aber keine Klasse), verwenden Sie o.__globals__ als Globale beim Aufruf von eval().

Allerdings können nicht alle als Annotationen verwendeten Zeichenketten von eval() erfolgreich in Python-Werte umgewandelt werden. Zeichenketten können theoretisch jede gültige Zeichenkette enthalten, und in der Praxis gibt es gültige Anwendungsfälle für Typ-Hints, die das Annotieren mit Zeichenketten erfordern, die speziell *nicht* ausgewertet werden können. Zum Beispiel

  • PEP 604 Union-Typen mit |, bevor die Unterstützung dafür in Python 3.10 hinzugefügt wurde.

  • Definitionen, die zur Laufzeit nicht benötigt werden, sondern nur importiert werden, wenn typing.TYPE_CHECKING wahr ist.

Wenn eval() versucht, solche Werte auszuwerten, schlägt er fehl und löst eine Ausnahme aus. Daher wird bei der Gestaltung einer Bibliotheks-API, die mit Annotationen arbeitet, empfohlen, nur dann zu versuchen, Zeichenketten auszuwerten, wenn der Aufrufer dies explizit anfordert.

Best Practices für __annotations__ in jeder Python-Version

  • Sie sollten direkte Zuweisungen zum __annotations__ Member von Objekten vermeiden. Lassen Sie Python die Zuweisung von __annotations__ verwalten.

  • Wenn Sie direkt dem __annotations__ Member eines Objekts zuweisen, sollten Sie es immer auf ein dict Objekt setzen.

  • Sie sollten vermeiden, __annotations__ direkt auf irgendeinem Objekt zuzugreifen. Verwenden Sie stattdessen annotationlib.get_annotations() (Python 3.14+) oder inspect.get_annotations() (Python 3.10+).

  • Wenn Sie direkt auf das __annotations__ Member eines Objekts zugreifen, sollten Sie sicherstellen, dass es sich um ein Dictionary handelt, bevor Sie versuchen, seinen Inhalt zu untersuchen.

  • Sie sollten das Ändern von __annotations__ Dictionaries vermeiden.

  • Sie sollten das Löschen des __annotations__ Attributs eines Objekts vermeiden.

__annotations__ Quirks

In allen Versionen von Python 3 erstellen Funktionsobjekte ein Annotations-Dictionary bei Bedarf, wenn keine Annotationen auf diesem Objekt definiert sind. Sie können das __annotations__ Attribut mit del fn.__annotations__ löschen, aber wenn Sie dann auf fn.__annotations__ zugreifen, erstellt das Objekt ein neues leeres Dictionary, das es speichert und als seine Annotationen zurückgibt. Das Löschen der Annotationen einer Funktion, bevor sie ihr Annotations-Dictionary bedarfsgesteuert erstellt hat, löst einen AttributeError aus; das zweimalige Hintereinander-Löschen von Annotationen mit del fn.__annotations__ garantiert immer einen AttributeError.

Alles im obigen Absatz gilt auch für Klassen- und Modulobjekte in Python 3.10 und neuer.

In allen Versionen von Python 3 können Sie __annotations__ für ein Funktionsobjekt auf None setzen. Allerdings wird der anschließende Zugriff auf die Annotationen dieses Objekts über fn.__annotations__ ein leeres Dictionary bei Bedarf erstellen, wie im ersten Absatz dieses Abschnitts beschrieben. Dies gilt *nicht* für Module und Klassen in irgendeiner Python-Version; diese Objekte erlauben das Setzen von __annotations__ auf jeden Python-Wert und behalten den gesetzten Wert.

Wenn Python Ihre Annotationen für Sie stringifiziert (mit from __future__ import annotations) und Sie eine Zeichenkette als Annotation angeben, wird die Zeichenkette selbst zitiert. Effektiv ist die Annotation *zweimal* zitiert. Zum Beispiel

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

Dies gibt {'a': "'str'"} aus. Dies sollte eigentlich nicht als "Quirk" betrachtet werden; es wird hier nur erwähnt, weil es überraschend sein könnte.

Wenn Sie eine Klasse mit einer benutzerdefinierten Metaklasse verwenden und auf __annotations__ der Klasse zugreifen, können Sie unerwartetes Verhalten beobachten; siehe 749 für einige Beispiele. Sie können diese Quirks vermeiden, indem Sie annotationlib.get_annotations() auf Python 3.14+ oder inspect.get_annotations() auf Python 3.10+ verwenden. Auf früheren Python-Versionen können Sie diese Fehler vermeiden, indem Sie auf die Annotationen aus dem __dict__ der Klasse zugreifen (z. B. cls.__dict__.get('__annotations__', None)).

In einigen Python-Versionen können Instanzen von Klassen ein __annotations__ Attribut haben. Dies ist jedoch keine unterstützte Funktionalität. Wenn Sie die Annotationen einer Instanz benötigen, können Sie type() verwenden, um auf ihre Klasse zuzugreifen (z. B. annotationlib.get_annotations(type(myinstance)) auf Python 3.14+).