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

Python Enhancement Proposals

PEP 593 – Flexible function and variable annotations

Autor:
Till Varoquaux <till at fb.com>, Konstantin Kashin <kkashin at fb.com>
Sponsor:
Ivan Levkivskyi <levkivskyi at gmail.com>
Discussions-To:
Typing-SIG list
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
26-Apr-2019
Python-Version:
3.9
Post-History:
20-May-2019

Inhaltsverzeichnis

Wichtig

Diese PEP ist ein historisches Dokument: siehe Annotated und typing.Annotated für aktuelle Spezifikationen und Dokumentation. Kanonische Typ-Spezifikationen werden auf der Typ-Spezifikations-Website gepflegt; das Laufzeitverhalten von Typen wird in der CPython-Dokumentation beschrieben.

×

Siehe den Prozess zur Aktualisierung der Typ-Spezifikation, um Änderungen an der Typ-Spezifikation vorzuschlagen.

Zusammenfassung

Diese PEP führt einen Mechanismus ein, um die Typ-Annotationen aus PEP 484 um beliebige Metadaten zu erweitern.

Motivation

PEP 484 bietet eine Standardsemantik für die in PEP 3107 eingeführten Annotationen. PEP 484 ist präskriptiv, aber der De-facto-Standard für die meisten Verbraucher von Annotationen; in vielen statisch überprüften Codebasen, in denen Typ-Annotationen weit verbreitet sind, haben sie effektiv jede andere Form von Annotation verdrängt. Einige der in PEP 3107 beschriebenen Anwendungsfälle für Annotationen (Datenbank-Mapping, Brücken für Fremdsprachen) sind angesichts der Verbreitung von Typ-Annotationen derzeit unrealistisch. Darüber hinaus schließt die Standardisierung von Typ-Annotationen fortgeschrittene Funktionen aus, die nur von bestimmten Typ-Prüfern unterstützt werden.

Begründung

Diese PEP fügt dem `typing`-Modul einen Typ namens Annotated hinzu, um bestehende Typen mit kontextspezifischen Metadaten zu dekorieren. Insbesondere kann ein Typ T mit Metadaten x über den Typehint Annotated[T, x] annotiert werden. Diese Metadaten können entweder zur statischen Analyse oder zur Laufzeit verwendet werden. Wenn eine Bibliothek (oder ein Werkzeug) einen Typehint Annotated[T, x] findet und keine spezielle Logik für Metadaten x hat, sollte sie diese ignorieren und den Typ einfach als T behandeln. Im Gegensatz zur no_type_check-Funktionalität, die derzeit im typing-Modul existiert und die Typüberprüfung von Annotationen für eine Funktion oder Klasse vollständig deaktiviert, ermöglicht der Typ Annotated sowohl die statische Typüberprüfung von T (z. B. über mypy oder Pyre, die x sicher ignorieren können) als auch den Laufzeitzugriff auf x innerhalb einer bestimmten Anwendung. Die Einführung dieses Typs würde eine Vielzahl von Anwendungsfällen von Interesse für die breitere Python-Community adressieren.

Dies wurde ursprünglich als Issue 600 im Typing-GitHub angesprochen und dann in Python-Ideen diskutiert.

Motivierende Beispiele

Kombination von Laufzeit- und statischen Verwendungen von Annotationen

Es gibt einen aufkommenden Trend, dass Bibliotheken Typ-Annotationen zur Laufzeit nutzen (z. B. `dataclasses`); die Möglichkeit, Typ-Annotationen um externe Daten zu erweitern, wäre ein großer Vorteil für diese Bibliotheken.

Hier ist ein Beispiel, wie ein hypothetisches Modul Annotationen nutzen könnte, um C-Strukturen zu lesen

UnsignedShort = Annotated[int, struct2.ctype('H')]
SignedChar = Annotated[int, struct2.ctype('b')]

class Student(struct2.Packed):
    # mypy typechecks 'name' field as 'str'
    name: Annotated[str, struct2.ctype("<10s")]
    serialnum: UnsignedShort
    school: SignedChar

# 'unpack' only uses the metadata within the type annotations
Student.unpack(record)
# Student(name=b'raymond   ', serialnum=4658, school=264)

Senkung der Hürden für die Entwicklung neuer Typkonstrukte

Typischerweise muss ein Entwickler bei der Hinzufügung eines neuen Typs diesen Typ in das `typing`-Modul hochladen und `mypy`, PyCharm, Pyre, pytype usw. ändern. Dies ist besonders wichtig, wenn man an Open-Source-Code arbeitet, der diese Typen verwendet, da der Code ohne zusätzliche Logik nicht sofort auf die Werkzeuge anderer Entwickler übertragbar wäre. Infolgedessen ist die Entwicklung und das Ausprobieren neuer Typen in einer Codebasis mit hohen Kosten verbunden. Idealerweise sollten Autoren neue Typen so einführen können, dass eine stufenweise Herabstufung möglich ist (z. B. wenn Clients keinen benutzerdefinierten mypy-Plugin haben), was die Entwicklungshürde senkt und ein gewisses Maß an Abwärtskompatibilität gewährleistet.

Angenommen, ein Autor möchte Unterstützung für Tagged Unions in Python hinzufügen. Eine Möglichkeit, dies zu erreichen, wäre, TypedDict in Python zu annotieren, sodass nur ein Feld gesetzt werden darf

Currency = Annotated[
    TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False),
    TaggedUnion,
]

Dies ist eine etwas umständliche Syntax, aber sie ermöglicht es uns, an diesem Proof-of-Concept zu iterieren und Personen mit Typ-Prüfern (oder anderen Werkzeugen) arbeiten zu lassen, die diese Funktion noch nicht unterstützen, in einer Codebasis mit Tagged Unions. Der Autor könnte diesen Vorschlag leicht testen und die Fehler ausbügeln, bevor er versucht, Tagged Unions in typing, mypy usw. hochzuladen. Darüber hinaus könnten Werkzeuge, die keine Unterstützung für das Parsen der TaggedUnion-Annotation haben, Currency weiterhin als TypedDict behandeln, was immer noch eine gute Annäherung darstellt (etwas weniger streng).

Spezifikation

Syntax

Annotated wird mit einem Typ und einer beliebigen Liste von Python-Werten parametrisiert, die die Annotationen darstellen. Hier sind die spezifischen Details der Syntax

  • Das erste Argument für Annotated muss ein gültiger Typ sein
  • Mehrere Typ-Annotationen werden unterstützt ( Annotated unterstützt variable Argumente)
    Annotated[int, ValueRange(3, 10), ctype("char")]
    
  • Annotated muss mit mindestens zwei Argumenten aufgerufen werden ( Annotated[int] ist nicht gültig)
  • Die Reihenfolge der Annotationen wird beibehalten und ist für Gleichheitsprüfungen relevant
    Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[
        int, ctype("char"), ValueRange(3, 10)
    ]
    
  • Verschachtelte Annotated-Typen werden abgeflacht, wobei die Metadaten mit der innersten Annotation beginnen
    Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
        int, ValueRange(3, 10), ctype("char")
    ]
    
  • Duplizierte Annotationen werden nicht entfernt
    Annotated[int, ValueRange(3, 10)] != Annotated[
        int, ValueRange(3, 10), ValueRange(3, 10)
    ]
    
  • Annotated kann mit verschachtelten und generischen Aliassen verwendet werden
    Typevar T = ...
    Vec = Annotated[List[Tuple[T, T]], MaxLen(10)]
    V = Vec[int]
    
    V == Annotated[List[Tuple[int, int]], MaxLen(10)]
    

Verbrauch von Annotationen

Letztendlich liegt die Verantwortung dafür, wie die Annotationen interpretiert werden (falls überhaupt), beim Werkzeug oder der Bibliothek, die auf den Annotated-Typ stößt. Ein Werkzeug oder eine Bibliothek, die auf einen Annotated-Typ stößt, kann die Annotationen durchsuchen, um festzustellen, ob sie von Interesse sind (z. B. mittels isinstance()).

Unbekannte Annotationen: Wenn ein Werkzeug oder eine Bibliothek Annotationen nicht unterstützt oder auf eine unbekannte Annotation stößt, sollte es diese einfach ignorieren und den annotierten Typ als zugrundeliegenden Typ behandeln. Wenn beispielsweise eine Annotation auf den Namen stößt, die keine Instanz von struct2.ctype ist (z. B. Annotated[str, 'foo', struct2.ctype("<10s")]), sollte die Entpackmethode diese ignorieren.

Namespaces für Annotationen: Namensräume sind für Annotationen nicht erforderlich, da die von den Annotationen verwendete Klasse als Namespace fungiert.

Mehrere Annotationen: Es liegt an dem Werkzeug, das die Annotationen konsumiert, zu entscheiden, ob der Client mehrere Annotationen für einen Typ haben darf und wie diese Annotationen zusammengeführt werden.

Da der Typ Annotated es ermöglicht, mehrere Annotationen desselben (oder verschiedener) Typs auf jedem Knoten anzubringen, sind die Werkzeuge oder Bibliotheken, die diese Annotationen konsumieren, dafür verantwortlich, mit möglichen Duplikaten umzugehen. Zum Beispiel könnten Sie bei der Wertebereichsanalyse Folgendes zulassen

T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]

Abflachung von verschachtelten Annotationen, dies führt zu

T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)]

Interaktion mit get_type_hints()

typing.get_type_hints() erhält ein neues Argument include_extras, das standardmäßig auf False gesetzt ist, um die Abwärtskompatibilität zu wahren. Wenn include_extras False ist, werden die zusätzlichen Annotationen aus dem Rückgabewert entfernt. Andernfalls werden die Annotationen unverändert zurückgegeben.

@struct2.packed
class Student(NamedTuple):
    name: Annotated[str, struct.ctype("<10s")]

get_type_hints(Student) == {'name': str}
get_type_hints(Student, include_extras=False) == {'name': str}
get_type_hints(Student, include_extras=True) == {
    'name': Annotated[str, struct.ctype("<10s")]
}

Aliase & Bedenken wegen Ausführlichkeit

Das Schreiben von typing.Annotated überall kann ziemlich umständlich sein; glücklicherweise bedeutet die Möglichkeit, Annotationen zu aliasieren, dass wir in der Praxis nicht erwarten, dass Kunden viel Boilerplate-Code schreiben müssen.

T = TypeVar('T')
Const = Annotated[T, my_annotations.CONST]

class C:
    def const_method(self: Const[List[int]]) -> int:
        ...

Abgelehnte Ideen

Einige der vorgeschlagenen Ideen wurden aus dieser PEP abgelehnt, da sie dazu führen würden, dass Annotated nicht sauber mit den anderen Typ-Annotationen integriert werden kann.

  • Annotated kann den dekorierten Typ nicht ableiten. Man könnte sich vorstellen, dass Annotated[..., Immutable] verwendet werden könnte, um einen Wert als unveränderlich zu markieren und dennoch seinen Typ abzuleiten. Typing unterstützt die Verwendung des abgeleiteten Typs nirgends sonst; es ist am besten, dies nicht als Sonderfall hinzuzufügen.
  • Verwendung von (Type, Ann1, Ann2, ...) anstelle von Annotated[Type, Ann1, Ann2, ...]. Dies würde zu Verwirrung führen, wenn Annotationen an verschachtelten Positionen auftreten (Callable[[A, B], C] ist zu ähnlich zu Callable[[(A, B)], C]) und würde es Konstruktoren unmöglich machen, als Passthrough zu fungieren (T(5) == C(5) wenn C = Annotation[T, Ann]).

Dieses Feature wurde weggelassen, um das Design einfach zu halten

  • Annotated kann nicht mit einem einzigen Argument aufgerufen werden. `Annotated` könnte den zugrundeliegenden Wert zurückgeben, wenn es mit einem einzigen Argument aufgerufen wird (z. B.: Annotated[int] == int). Dies verkompliziert die Spezifikationen und bietet wenig Nutzen.

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

Zuletzt geändert: 2024-06-11 22:12:09 GMT