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

Python Enhancement Proposals

PEP 563 – Nachträgliche Auswertung von Annotationen

Autor:
Łukasz Langa <lukasz at python.org>
Discussions-To:
Python-Dev Liste
Status:
Abgelöst
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
08-Sep-2017
Python-Version:
3.7
Post-History:
01. Nov. 2017, 21. Nov. 2017
Ersetzt-Durch:
649, 749
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Entscheidung

Die in dieser PEP vorgeschlagenen Funktionen sind nie zum Standardverhalten geworden und wurden durch die verzögerte Auswertung von Annotationen ersetzt, wie sie von PEP 649 und PEP 749 vorgeschlagen wurden.

Zusammenfassung

PEP 3107 führte eine Syntax für Funktionsannotationen ein, aber die Semantik wurde bewusst undefiniert gelassen. PEP 484 führte eine Standardbedeutung für Annotationen ein: Typ-Hints. PEP 526 definierte Variablenannotationen und band sie explizit an den Typ-Hint-Anwendungsfall.

Diese PEP schlägt vor, Funktionsannotationen und Variablenannotationen so zu ändern, dass sie nicht mehr zum Zeitpunkt der Funktionsdefinition ausgewertet werden. Stattdessen werden sie in String-Form in __annotations__ beibehalten.

Diese Änderung wird schrittweise eingeführt, beginnend mit einem __future__-Import in Python 3.7.

Begründung und Ziele

PEP 3107 fügte Unterstützung für beliebige Annotationen für Teile einer Funktionsdefinition hinzu. Genau wie Standardwerte werden Annotationen zum Zeitpunkt der Funktionsdefinition ausgewertet. Dies schafft eine Reihe von Problemen für den Typ-Hint-Anwendungsfall

  • Vorwärtsreferenzen: Wenn ein Typ-Hint Namen enthält, die noch nicht definiert wurden, muss diese Definition als String-Literal ausgedrückt werden;
  • Typ-Hints werden zum Zeitpunkt des Modul-Imports ausgeführt, was nicht rechenfrei ist.

Das Aufschieben der Auswertung von Annotationen löst beide Probleme. HINWEIS: PEP 649 schlägt eine alternative Lösung für die oben genannten Probleme vor, was diese PEP in Gefahr bringt, überholt zu werden.

Non-goals

Wie bereits in PEP 484 und PEP 526 hervorgehoben wird, ist zu betonen, dass Python eine dynamisch typisierte Sprache bleibt und die Autoren keinesfalls beabsichtigen, Typ-Hints jemals obligatorisch zu machen, auch nicht durch Konvention.

Diese PEP soll das Problem der Vorwärtsreferenzen in Typ-Annotationen lösen. Es gibt noch Fälle außerhalb von Annotationen, in denen Vorwärtsreferenzen die Verwendung von String-Literalen erfordern werden. Diese sind in einem späteren Abschnitt dieses Dokuments aufgeführt.

Annotationen ohne erzwungene Auswertung eröffnen Möglichkeiten zur Verbesserung der Syntax von Typ-Hints. Diese Idee erfordert eine eigene separate PEP und wird in diesem Dokument nicht weiter diskutiert.

Nicht-Typen-Nutzung von Annotationen

Während Annotationen weiterhin für beliebige Zwecke neben der Typüberprüfung verfügbar sind, ist es erwähnenswert, dass das Design dieser PEP sowie seiner Vorgänger (PEP 484 und PEP 526) vorwiegend durch den Typ-Hint-Anwendungsfall motiviert ist.

In Python 3.8 wird PEP 484 vom provisorischen Status zu einem stabilen Status übergehen. Andere Erweiterungen der Python-Programmiersprache wie PEP 544, PEP 557 oder PEP 560 bauen bereits darauf auf, da sie von Typ-Annotationen und dem typing-Modul abhängen, wie es von PEP 484 definiert wurde. Tatsächlich ist der Grund, warum PEP 484 in Python 3.7 provisorisch bleibt, die Ermöglichung einer schnellen Weiterentwicklung für einen weiteren Release-Zyklus, den einige der oben genannten Erweiterungen benötigen.

Vor diesem Hintergrund sollten Verwendungen von Annotationen, die mit den genannten PEPs inkompatibel sind, als veraltet betrachtet werden.

Implementierung

Mit dieser PEP werden Funktions- und Variablenannotationen nicht mehr zum Definitionszeitpunkt ausgewertet. Stattdessen wird eine String-Form im jeweiligen __annotations__-Dictionary gespeichert. Statische Typüberprüfer werden keinen Unterschied im Verhalten feststellen, während Werkzeuge, die Annotationen zur Laufzeit verwenden, eine nachträgliche Auswertung durchführen müssen.

Die String-Form wird während des Kompilierungsschritts aus dem AST gewonnen, was bedeutet, dass die String-Form möglicherweise nicht die exakte Formatierung des Quellcodes beibehält. Hinweis: Wenn eine Annotation bereits ein String-Literal war, wird sie immer noch in einen String eingeschlossen.

Annotationen müssen syntaktisch gültige Python-Ausdrücke sein, auch wenn sie als Literal-Strings übergeben werden (d.h. compile(literal, '', 'eval')). Annotationen können nur Namen verwenden, die im Modul-Scope vorhanden sind, da die nachträgliche Auswertung mit lokalen Namen nicht zuverlässig ist (mit der einzigen Ausnahme von Klassenebene-Namen, die von typing.get_type_hints() aufgelöst werden).

Beachten Sie, dass gemäß PEP 526 lokale Variablenannotationen überhaupt nicht ausgewertet werden, da sie außerhalb des Funktions-Closures nicht zugänglich sind.

Aktivierung des zukünftigen Verhaltens in Python 3.7

Die oben beschriebene Funktionalität kann ab Python 3.7 mit dem folgenden speziellen Import aktiviert werden

from __future__ import annotations

Eine Referenzimplementierung dieser Funktionalität ist auf GitHub verfügbar.

Auflösung von Typ-Hints zur Laufzeit

Um eine Annotation zur Laufzeit von ihrer String-Form zum Ergebnis des umschließenden Ausdrucks aufzulösen, muss der Benutzer-Code den String auswerten.

Für Code, der Typ-Hints verwendet, wertet die Funktion typing.get_type_hints(obj, globalns=None, localns=None) Ausdrücke korrekt aus ihrer String-Form zurück. Beachten Sie, dass jeder gültige Code, der derzeit __annotations__ verwendet, dies bereits tun sollte, da eine Typ-Annotation als String-Literal ausgedrückt werden kann.

Für Code, der Annotationen für andere Zwecke verwendet, reicht ein normaler eval(ann, globals, locals) Aufruf aus, um die Annotation aufzulösen.

In beiden Fällen ist es wichtig, zu berücksichtigen, wie Globale und Lokale die verzögerte Auswertung beeinflussen. Eine Annotation wird nicht mehr zum Zeitpunkt der Definition und, was noch wichtiger ist, im selben Scope ausgewertet, in dem sie definiert wurde. Folglich ist die Verwendung von lokalem Zustand in Annotationen im Allgemeinen nicht mehr möglich. Was Globale betrifft, so ist das Modul, in dem die Annotation definiert wurde, der korrekte Kontext für die verzögerte Auswertung.

Die Funktion get_type_hints() löst automatisch den korrekten Wert von globalns für Funktionen und Klassen auf. Sie stellt auch automatisch das korrekte localns für Klassen bereit.

Bei der Ausführung von eval() kann der Wert von Globals auf folgende Weise gesammelt werden:

  • Funktions-Objekte halten eine Referenz auf ihre jeweiligen Globals in einem Attribut namens __globals__;
  • Klassen halten den Namen des Moduls, in dem sie definiert wurden, dies kann verwendet werden, um die jeweiligen Globals abzurufen
    cls_globals = vars(sys.modules[SomeClass.__module__])
    

    Beachten Sie, dass dies für Basisklassen wiederholt werden muss, um alle __annotations__ auszuwerten.

  • Module sollten ihr eigenes __dict__ verwenden.

Der Wert von localns kann für Funktionen nicht zuverlässig abgerufen werden, da der Stack-Frame zum Zeitpunkt des Aufrufs höchstwahrscheinlich nicht mehr existiert.

Für Klassen kann localns durch Verketten von Variablen der gegebenen Klasse und ihrer Basisklassen (in der Methodenauflösungsreihenfolge) zusammengesetzt werden. Da Slots erst nach der Klassendefinition gefüllt werden können, müssen wir sie für diesen Zweck nicht berücksichtigen.

Auflösung von Annotationen zur Laufzeit und Klassen-Dekoratoren

Metaklassen und Klassen-Dekoratoren, die Annotationen für die aktuelle Klasse auflösen müssen, schlagen fehl für Annotationen, die den Namen der aktuellen Klasse verwenden. Beispiel

def class_decorator(cls):
    annotations = get_type_hints(cls)  # raises NameError on 'C'
    print(f'Annotations for {cls}: {annotations}')
    return cls

@class_decorator
class C:
    singleton: 'C' = None

Dies war bereits vor dieser PEP der Fall. Der Klassen-Dekorator wirkt auf die Klasse, bevor sie einen Namen im aktuellen Definitions-Scope erhält.

Auflösung von Annotationen zur Laufzeit und TYPE_CHECKING

Manchmal gibt es Code, der von einem Typüberprüfer gesehen werden muss, aber nicht ausgeführt werden soll. Für solche Situationen definiert das typing-Modul eine Konstante, TYPE_CHECKING, die während der Typüberprüfung als True, aber zur Laufzeit als False betrachtet wird. Beispiel

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: expensive_mod.SomeClass) -> None:
    a_var: expensive_mod.SomeClass = arg
    ...

Dieser Ansatz ist auch nützlich bei der Handhabung von Importzyklen.

Der Versuch, Annotationen von a_func zur Laufzeit mit typing.get_type_hints() aufzulösen, schlägt fehl, da der Name expensive_mod nicht definiert ist (die Variable TYPE_CHECKING ist zur Laufzeit False). Dies war bereits vor dieser PEP der Fall.

Abwärtskompatibilität

Dies ist eine abwärtsinkompatible Änderung. Anwendungen, die von beliebigen Objekten abhängig sind, die direkt in Annotationen vorhanden sein sollen, brechen, wenn sie nicht typing.get_type_hints() oder eval() verwenden.

Annotationen, die von Locals zum Zeitpunkt der Funktionsdefinition abhängen, werden später nicht auflösbar sein. Beispiel

def generate():
    A = Optional[int]
    class C:
        field: A = 1
        def method(self, arg: A) -> None: ...
    return C
X = generate()

Der Versuch, Annotationen von X später mit get_type_hints(X) aufzulösen, schlägt fehl, da A und sein umschließender Scope nicht mehr existieren. Python wird keine Anstalten machen, solche Annotationen zu verbieten, da sie oft immer noch erfolgreich statisch analysiert werden können, was der vorherrschende Anwendungsfall für Annotationen ist.

Annotationen, die verschachtelte Klassen und deren Zustand verwenden, sind weiterhin gültig. Sie können lokale Namen oder den vollständig qualifizierten Namen verwenden. Beispiel

class C:
    field = 'c_field'
    def method(self) -> C.field:  # this is OK
        ...

    def method(self) -> field:  # this is OK
        ...

    def method(self) -> C.D:  # this is OK
        ...

    def method(self) -> D:  # this is OK
        ...

    class D:
        field2 = 'd_field'
        def method(self) -> C.D.field2:  # this is OK
            ...

        def method(self) -> D.field2:  # this FAILS, class D is local to C
            ...                        # and is therefore only available
                                       # as C.D. This was already true
                                       # before the PEP.

        def method(self) -> field2:  # this is OK
            ...

        def method(self) -> field:  # this FAILS, field is local to C and
                                    # is therefore not visible to D unless
                                    # accessed as C.field. This was already
                                    # true before the PEP.

Bei einer Annotation, die kein syntaktisch gültiger Ausdruck ist, wird zur Kompilierzeit ein SyntaxError ausgelöst. Da Namen jedoch zu diesem Zeitpunkt nicht aufgelöst werden, wird keine Überprüfung vorgenommen, ob die verwendeten Namen korrekt sind oder nicht.

Deprecations-Richtlinie

Ab Python 3.7 ist ein __future__-Import erforderlich, um die beschriebene Funktionalität zu nutzen. Es werden keine Warnungen ausgegeben.

HINWEIS: Ob dies schließlich zum Standardverhalten wird, ist derzeit unklar und hängt von der Entscheidung zu PEP 649 ab. In jedem Fall ist die Verwendung von Annotationen, die von ihrer sofortigen Auswertung abhängen, mit beiden Vorschlägen inkompatibel und wird nicht mehr unterstützt.

Vorwärtsreferenzen

Die absichtliche Verwendung eines Namens, bevor er im Modul definiert wurde, wird als Vorwärtsreferenz bezeichnet. Für die Zwecke dieses Abschnitts bezeichnen wir auch jeden Namen, der innerhalb eines if TYPE_CHECKING:-Blocks importiert oder definiert wurde, als Vorwärtsreferenz.

Diese PEP befasst sich mit dem Problem der Vorwärtsreferenzen in Typ-Annotationen. Die Verwendung von String-Literalen ist in diesem Fall nicht mehr erforderlich. Es gibt jedoch APIs im typing-Modul, die andere syntaktische Konstrukte der Sprache verwenden, und diese erfordern weiterhin die Umgehung von Vorwärtsreferenzen mit String-Literalen. Die Liste umfasst

  • Typdefinitionen
    T = TypeVar('T', bound='<type>')
    UserId = NewType('UserId', '<type>')
    Employee = NamedTuple('Employee', [('name', '<type>'), ('id', '<type>')])
    
  • Aliase
    Alias = Optional['<type>']
    AnotherAlias = Union['<type>', '<type>']
    YetAnotherAlias = '<type>'
    
  • Casting
    cast('<type>', value)
    
  • Basisklassen
    class C(Tuple['<type>', '<type>']): ...
    

Je nach spezifischem Fall können einige der oben genannten Fälle durch Platzierung der Verwendung in einem if TYPE_CHECKING:-Block umgangen werden. Dies funktioniert nicht für Code, der zur Laufzeit verfügbar sein muss, insbesondere für Basisklassen und Casting. Für Named-Tuples löst die Verwendung der neuen Klassendefinitionssyntax, die in Python 3.6 eingeführt wurde, das Problem.

Im Allgemeinen erfordert die Behebung des Problems für alle Vorwärtsreferenzen eine Änderung der Art und Weise, wie Module in Python instanziiert werden, vom aktuellen Ein-Pass-Top-Down-Modell abweichend. Dies wäre eine große Änderung in der Sprache und liegt außerhalb des Umfangs dieser PEP.

Abgelehnte Ideen

Beibehaltung der Möglichkeit, lokalen Funktionszustand bei der Definition von Annotationen zu verwenden

Bei verzögerter Auswertung würde dies erfordern, eine Referenz auf den Frame zu behalten, in dem eine Annotation erstellt wurde. Dies könnte zum Beispiel erreicht werden, indem alle Annotationen als Lambdas anstelle von Strings gespeichert werden.

Dies wäre für stark annotierten Code prohibitiv teuer, da die Frames alle ihre Objekte am Leben halten würden. Dies schließt vor allem Objekte ein, auf die nie wieder zugegriffen wird.

Um den Gültigkeitsbereich auf Klassenebene adressieren zu können, würde der Lambda-Ansatz eine neue Art von Zelle im Interpreter erfordern. Dies würde die Anzahl der Typen, die in __annotations__ erscheinen können, vermehren und wäre nicht so introspektiv wie Strings.

Beachten Sie, dass im Falle von verschachtelten Klassen die Funktionalität zum Abrufen der effektiven "Globals" und "Locals" zum Zeitpunkt der Definition von typing.get_type_hints() bereitgestellt wird.

Wenn eine Funktion eine Klasse oder Funktion mit Annotationen generiert, die lokale Variablen verwenden müssen, kann sie das __annotations__-Dictionary des gegebenen generierten Objekts direkt befüllen, ohne sich auf den Compiler zu verlassen.

Verbot der Nutzung lokalen Zustands auch für Klassen

Diese PEP schlug ursprünglich vor, Namen innerhalb von Annotationen nur für Namen aus dem Modul-Scope zuzulassen, auch für Klassen. Der Autor argumentierte, dass dies die Namensauflösung eindeutig mache, auch in Fällen von Konflikten zwischen lokalen Namen und Modul-Scope-Namen.

Diese Idee wurde schließlich im Fall von Klassen abgelehnt. Stattdessen wurde typing.get_type_hints() modifiziert, um den lokalen Namensraum korrekt zu befüllen, wenn Annotationen auf Klassenebene benötigt werden.

Die Gründe für die Ablehnung der Idee waren, dass sie gegen die Intuition der Funktionsweise von Scopes in Python verstößt und zu viele bestehende Typ-Annotationen brechen würde, um den Übergang mühsam zu gestalten. Schließlich ist der Zugriff auf den lokalen Scope für Klassen-Dekoratoren erforderlich, um Typ-Annotationen auswerten zu können. Dies liegt daran, dass Klassen-Dekoratoren angewendet werden, bevor die Klasse ihren Namen im äußeren Scope erhält.

Einführung eines neuen Dictionaries für die String-Literal-Form stattdessen

Yury Selivanov teilte die folgende Idee

  1. Füge eine neue spezielle Attribut zu Funktionen hinzu: __annotations_text__.
  2. Mache __annotations__ zu einem Lazy-Dynamik-Mapping, das Ausdrücke aus dem entsprechenden Schlüssel in __annotations_text__ Just-in-time auswertet.

Diese Idee soll das Problem der Abwärtskompatibilität lösen und die Notwendigkeit eines neuen __future__-Imports beseitigen. Leider ist das nicht genug. Die verzögerte Auswertung ändert, auf welchen Zustand die Annotation Zugriff hat. Während die verzögerte Auswertung das Vorwärtsreferenzproblem löst, macht sie auch den Zugriff auf lokale Funktionsvariablen unmöglich. Dies allein ist eine Quelle für Abwärtsinkompatibilität, die eine Deprecations-Periode rechtfertigt.

Ein __future__-Import ist ein offensichtlicher und expliziter Indikator für die Zustimmung zur neuen Funktionalität. Er macht es auch externen Werkzeugen trivial, den Unterschied zwischen einer Python-Datei, die den alten oder den neuen Ansatz verwendet, zu erkennen. Im ersteren Fall würde dieses Werkzeug erkennen, dass der Zugriff auf lokalen Zustand erlaubt ist, während es im letzteren Fall erkennen würde, dass Vorwärtsreferenzen erlaubt sind.

Schließlich ist eine Just-in-time-Auswertung in __annotations__ ein unnötiger Schritt, wenn get_type_hints() später verwendet wird.

Entfernen von Annotationen mit -O

Es gibt zwei Gründe, warum dies für die Zwecke dieser PEP nicht zufriedenstellend ist.

Erstens behandelt dies nur die Kosten zur Laufzeit, nicht die Vorwärtsreferenzen, diese können immer noch nicht sicher im Quellcode verwendet werden. Ein Bibliotheksentwickler könnte niemals Vorwärtsreferenzen verwenden, da dies die Benutzer der Bibliothek zwingen würde, diesen neuen hypothetischen -O-Schalter zu verwenden.

Zweitens wirft dies das Kind mit dem Bade aus. Jetzt kann keine Annotation mehr zur Laufzeit verwendet werden. PEP 557 ist ein Beispiel für eine neuere Entwicklung, bei der die Auswertung von Typ-Annotationen zur Laufzeit nützlich ist.

All das gesagt, ist eine granulare -O-Option zum Verwerfen von Annotationen eine Möglichkeit in der Zukunft, da sie konzeptionell mit dem bestehenden -O-Verhalten (Verwerfen von Docstrings und Assertionsanweisungen) kompatibel ist. Diese PEP macht die Idee nicht ungültig.

Übergabe von String-Literalen in Annotationen unverändert an __annotations__

Diese PEP schlug ursprünglich vor, den Inhalt eines String-Literals direkt unter seinem jeweiligen Schlüssel in __annotations__ zu speichern. Dies sollte die Unterstützung für Laufzeit-Typüberprüfer vereinfachen.

Mark Shannon wies darauf hin, dass diese Idee fehlerhaft sei, da sie Situationen nicht berücksichtigte, in denen Strings nur ein Teil einer Typ-Annotation sind.

Die Inkonsistenz davon war immer offensichtlich, aber da sie Fälle von doppelt verpackten Strings nicht vollständig verhindert, lohnt es sich nicht.

Mache den Namen des zukünftigen Imports aussagekräftiger

Anstatt den folgenden Import zu benötigen

from __future__ import annotations

könnte die PEP die Funktion expliziter aufrufen, zum Beispiel string_annotations, stringify_annotations, annotation_strings, annotations_as_strings, lazy_annotations, static_annotations, usw.

Das Problem mit diesen Namen ist, dass sie sehr langwierig sind. Jeder von ihnen, außer lazy_annotations, wäre der längste Zukunftsfunktionsname in Python. Sie sind lang zum Tippen und schwerer zu merken als die einwortige Form.

Es gibt Präzedenzfälle für einen Zukunftsimportnamen, der übermäßig generisch klingt, aber in der Praxis für Benutzer offensichtlich war, was er tut

from __future__ import division

Vorherige Diskussion

In PEP 484

Das Problem der Vorwärtsreferenz wurde diskutiert, als PEP 484 ursprünglich entworfen wurde, was zu folgender Aussage im Dokument führte

Ein Kompromiss ist möglich, bei dem ein __future__-Import alle Annotationen in einem bestimmten Modul in String-Literale umwandeln könnte, wie folgt
from __future__ import annotations

class ImSet:
    def add(self, a: ImSet) -> List[ImSet]: ...

assert ImSet.add.__annotations__ == {
    'a': 'ImSet', 'return': 'List[ImSet]'
}

Eine solche __future__-Importanweisung kann in einer separaten PEP vorgeschlagen werden.

python/typing#400

Das Problem wurde ausführlich im GitHub-Projekt des Typing-Moduls unter Issue 400 diskutiert. Die Problemstellung dort enthält Kritik an generischen Typen, die Importe aus typing erfordern. Dies ist für Anfänger oft verwirrend

Warum dies
from typing import List, Set
def dir(o: object = ...) -> List[str]: ...
def add_friends(friends: Set[Friend]) -> None: ...

Aber nicht dies

def dir(o: object = ...) -> list[str]: ...
def add_friends(friends: set[Friend]) -> None ...

Warum dies

up_to_ten = list(range(10))
friends = set()

Aber nicht dies

from typing import List, Set
up_to_ten = List[int](range(10))
friends = Set[Friend]()

Während die Benutzerfreundlichkeit von Typen ein interessantes Problem ist, liegt sie außerhalb des Umfangs dieser PEP. Insbesondere erfordern alle Erweiterungen der in PEP 484 standardisierten Tippsyntax ihre eigenen jeweiligen PEPs und Genehmigungen.

Issue 400 schlägt letztendlich vor, die Auswertung von Annotationen aufzuschieben und sie als Strings in __annotations__ zu belassen, genau wie diese PEP spezifiziert. Diese Idee wurde gut aufgenommen. Ivan Levkivskyi unterstützte die Verwendung des __future__-Imports und schlug vor, die AST in compile.c zu "unparsen". Jukka Lehtosalo wies darauf hin, dass es einige Fälle von Vorwärtsreferenzen gibt, in denen Typen außerhalb von Annotationen verwendet werden und die verzögerte Auswertung diesen nicht hilft. Für diese Fälle müsste weiterhin die String-Literal-Notation verwendet werden. Diese Fälle werden in dem Abschnitt "Vorwärtsreferenzen" dieser PEP kurz erörtert.

Die größte Kontroverse bei dem Problem war Guido van Rossums Bedenken, dass das "Entokenisieren" von Annotationsausdrücken zurück in ihre String-Form keine Präzedenzfälle in der Python-Programmiersprache hat und sich wie eine hacky-Workaround anfühlt. Er sagte

Eine Sache, die mir einfällt, ist, dass dies eine sehr zufällige Änderung an der Sprache ist. Es könnte nützlich sein, eine kompaktere Möglichkeit zu haben, die verzögerte Ausführung von Ausdrücken anzuzeigen (mit weniger Syntax als lambda:). Aber warum sollte der Anwendungsfall von Typ-Annotationen so wichtig sein, die Sprache zu ändern, um dies dort zuerst zu tun (anstatt eine allgemeinere Lösung vorzuschlagen), angesichts der Tatsache, dass es bereits eine Lösung für diesen speziellen Anwendungsfall gibt, die nur minimale Syntax erfordert?

Schließlich äußerten Ethan Smith und schollii, dass das gesammelte Feedback während der PyCon US darauf hindeutet, dass der Zustand der Vorwärtsreferenzen behoben werden muss. Guido van Rossum schlug vor, auf die __future__-Idee zurückzukommen und wies darauf hin, dass es wichtig ist, die Annotationen sowohl syntaktisch gültig als auch zur Laufzeit korrekt auswertbar zu halten, um Missbrauch zu verhindern.

Erste Diskussionsentwurf auf python-ideas

Die Diskussion fand hauptsächlich in zwei Threads statt, der ursprünglichen Ankündigung und einer Folge-Diskussion namens PEP 563 und teure Abwärtskompatibilität.

Die PEP erhielt eher warme Rückmeldungen (4 stark dafür, 2 dafür mit Bedenken, 2 dagegen). Die größte kritische Stimme in dem ersteren Thread war Steven D’Apranos Überprüfung, die besagte, dass die Problemdefinition der PEP keinen Bruch der Abwärtskompatibilität rechtfertigt. In dieser Antwort schien Steven sich hauptsächlich darüber Sorgen zu machen, dass Python die Auswertung von Annotationen, die von lokalen Funktions-/Klassen-Zuständen abhingen, nicht mehr unterstützt.

Einige Leute äußerten Bedenken, dass es Bibliotheken gibt, die Annotationen für nicht-typbezogene Zwecke verwenden. Allerdings würde keine der genannten Bibliotheken durch diese PEP ungültig werden. Sie erfordern eine Anpassung an die neue Anforderung, eval() auf die Annotation mit den korrekten globals und locals angewendet aufzurufen.

Dieses Detail, dass globals und locals korrekt sein müssen, wurde von einer Reihe von Kommentatoren aufgegriffen. Alyssa (Nick) Coghlan benchmarkte das Umwandeln von Annotationen in Lambdas anstelle von Strings, leider erwies sich dies zur Laufzeit als deutlich langsamer als die aktuelle Situation.

Der letztere Thread wurde von Jim J. Jewett initiiert, der betonte, dass die Fähigkeit zur ordnungsgemäßen Auswertung von Annotationen eine wichtige Anforderung ist und die Abwärtskompatibilität in dieser Hinsicht wertvoll ist. Nach einiger Diskussion gab er zu, dass Nebeneffekte in Annotationen ein Code-Geruch sind und eine modulare Unterstützung für die Auswertung oder Nicht-Auswertung eine unübersichtliche Lösung ist. Seine größte Sorge blieb der Verlust von Funktionalität, der aus den Auswertungsbeschränkungen für globale und lokale Bereiche resultierte.

Alyssa Coghlan wies darauf hin, dass einige dieser Auswertungsbeschränkungen aus der PEP durch eine clevere Implementierung einer Auswertungs-Hilfsfunktion aufgehoben werden könnten, die selbst-referenzierende Klassen auch in Form eines Klassen-Dekorators lösen könnte. Sie schlug vor, dass die PEP diese Hilfsfunktion in die Standardbibliothek aufnehmen sollte.

Zweiter Diskussionsentwurf auf python-dev

Die Diskussion fand hauptsächlich im Ankündigungs-Thread statt, gefolgt von einer kurzen Diskussion unter Mark Shannons Beitrag.

Steven D’Aprano war besorgt, ob Tippfehler in Annotationen nach der von der PEP vorgeschlagenen Änderung zulässig sind. Brett Cannon antwortete, dass Typüberprüfer und andere statische Analysetools (wie Linter oder Texteditoren) diese Art von Fehlern abfangen würden. Jukka Lehtosalo fügte hinzu, dass diese Situation analog dazu ist, wie Namen in Funktionskörpern erst aufgelöst werden, wenn die Funktion aufgerufen wird.

Ein Hauptdiskussionsthema war Alyssa Coghlan's Vorschlag, Annotationen in "Thunk-Form" zu speichern, das heißt als spezialisierte Lambda-Funktionen, die auf den Klassenebene-Scope zugreifen könnten (und eine Scope-Anpassung beim Aufruf erlauben würden). Er präsentierte ein mögliches Design dafür (indirekte Attribut-Zellen). Dies wurde später als äquivalent zu "special forms" in Lisp betrachtet. Guido van Rossum äußerte die Sorge, dass eine solche Funktion nicht in zwölf Wochen sicher implementiert werden könne (d.h. rechtzeitig vor dem Beta-Freeze von Python 3.7).

Nach einer Weile wurde klar, dass der Trennungspunkt zwischen Befürwortern der String-Form und Befürwortern der Thunk-Form eigentlich darin liegt, ob Annotationen als allgemeines syntaktisches Element wahrgenommen werden sollten oder als etwas, das an den Typüberprüfungs-Anwendungsfall gebunden ist.

Schließlich erklärte Guido van Rossum, dass er die Thunk-Idee ablehne, da sie einen neuen Baustein im Interpreter erfordern würde. Dieser Baustein würde in Annotationen freigelegt, was die Anzahl der Typen, die in __annotations__ gespeichert werden können, vervielfachen würde (beliebige Objekte, Strings und nun Thunks). Außerdem sind Thunks nicht so introspektiv wie Strings. Vor allem äußerte Guido van Rossum Interesse daran, die Verwendung von Annotationen schrittweise auf statische Typisierung zu beschränken (mit einer optionalen Laufzeitkomponente).

Alyssa Coghlan wurde ebenfalls von PEP 563 überzeugt und begann prompt die obligatorische "Bike Shedding"-Sitzung über den Namen des __future__-Imports. Viele Diskutanten stimmten zu, dass annotations ein zu breiter Name für den Feature-Namen zu sein schien. Guido van Rossum entschied kurzzeitig, ihn string_annotations zu nennen, änderte dann aber seine Meinung und argumentierte, dass division ein Präzedenzfall für einen breiten Namen mit klarer Bedeutung sei.

Die endgültige Verbesserung des PEP, die in der Diskussion von Mark Shannon vorgeschlagen wurde, war die Ablehnung der Versuchung, String-Literale unverändert an __annotations__ zu übergeben.

Ein Nebenfaden der Diskussion drehte sich um die Laufzeitkosten statischer Typisierung, mit Themen wie der Importzeit des typing-Moduls (das ohne Abhängigkeiten mit re vergleichbar ist und dreimal so schwer wie re ist, wenn Abhängigkeiten gezählt werden).

Danksagungen

Dieses Dokument konnte ohne wertvolle Beiträge, Ermutigung und Ratschläge von Guido van Rossum, Jukka Lehtosalo und Ivan Levkivskyi nicht fertiggestellt werden.

Die Implementierung wurde von Serhiy Storchaka gründlich überprüft, der alle Arten von Problemen fand, darunter Fehler, schlechte Lesbarkeit und Leistungsprobleme.


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

Zuletzt geändert: 2025-05-06 22:56:50 GMT