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

Python Enhancement Proposals

PEP 591 – Hinzufügen eines final-Qualifikators zu typing

Autor:
Michael J. Sullivan <sully at msully.net>, Ivan Levkivskyi <levkivskyi at gmail.com>
BDFL-Delegate:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG list
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
15. Mrz. 2019
Python-Version:
3.8
Post-History:

Resolution:
Typing-SIG Nachricht

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument: siehe @final/@typing.final und Final/typing.Final für aktuelle Spezifikationen und Dokumentation. Kanonische Typing-Spezifikationen werden auf der Website der Typing-Spezifikationen gepflegt; das Laufzeitverhalten von Typing wird in der CPython-Dokumentation beschrieben.

×

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

Zusammenfassung

Dieses PEP schlägt einen „final“-Qualifikator vor, der dem typing Modul hinzugefügt werden soll – in Form eines final Dekorators und einer Final Typannotation – um drei zusammenhängende Zwecke zu erfüllen

  • Deklarieren, dass eine Methode nicht überschrieben werden sollte
  • Deklarieren, dass eine Klasse nicht unterklassifiziert werden sollte
  • Deklarieren, dass eine Variable oder ein Attribut nicht neu zugewiesen werden sollte

Motivation

Der final Dekorator

Das aktuelle typing Modul hat keine Möglichkeit, die Verwendung von Vererbung oder Überschreibung auf Typüberprüfungsebene einzuschränken. Dies ist eine übliche Funktion in anderen objektorientierten Sprachen (wie Java) und nützlich, um den potenziellen Verhaltensraum einer Klasse zu reduzieren und die Argumentation zu erleichtern.

Einige Situationen, in denen eine finale Klasse oder Methode nützlich sein kann, umfassen

  • Eine Klasse wurde nicht zum Unterklassifizieren entworfen, oder eine Methode wurde nicht zum Überschreiben entworfen. Vielleicht würde sie nicht wie erwartet funktionieren oder fehleranfällig sein.
  • Unterklassifizieren oder Überschreiben würde den Code schwieriger zu verstehen oder zu warten machen. Zum Beispiel möchten Sie möglicherweise unnötig enge Kopplungen zwischen Basisklassen und Unterklassen verhindern.
  • Sie möchten sich die Freiheit behalten, die Klassenimplementierung zukünftig beliebig zu ändern, und diese Änderungen könnten Unterklassen brechen.

Die Final Annotation

Dem aktuellen typing Modul fehlt eine Möglichkeit anzuzeigen, dass einer Variablen kein Wert zugewiesen wird. Dies ist in mehreren Situationen eine nützliche Funktion

  • Verhindern der unbeabsichtigten Änderung von Modul- und Klassenebene-Konstanten und deren überprüfbare Dokumentation als Konstanten.
  • Erstellen eines schreibgeschützten Attributs, das von Unterklassen nicht überschrieben werden kann. (@property kann ein Attribut schreibgeschützt machen, verhindert aber kein Überschreiben)
  • Ermöglichen der Verwendung eines Namens in Situationen, in denen normalerweise ein Literal erwartet wird (z. B. als Feldname für NamedTuple, ein Tupel von Typen, die an isinstance übergeben werden, oder ein Argument für eine Funktion mit Argumenten vom Typ Literal (PEP 586)).

Spezifikation

Der final Dekorator

Der typing.final Dekorator wird verwendet, um die Verwendung von Vererbung und Überschreibung einzuschränken.

Ein Typüberprüfer sollte jede Klasse, die mit @final dekoriert ist, vom Unterklassifizieren und jede Methode, die mit @final dekoriert ist, vom Überschreiben in einer Unterklasse verbieten. Die Methoden-Dekoratorversion kann mit Instanzmethoden, Klassenmethoden, statischen Methoden und Eigenschaften verwendet werden.

Zum Beispiel:

from typing import final

@final
class Base:
    ...

class Derived(Base):  # Error: Cannot inherit from final class "Base"
    ...

und

from typing import final

class Base:
    @final
    def foo(self) -> None:
        ...

class Derived(Base):
    def foo(self) -> None:  # Error: Cannot override final attribute "foo"
                            # (previously declared in base class "Base")
        ...

Für überladene Methoden sollte @final auf der Implementierung platziert werden (oder auf der ersten Überladung für Stubs)

from typing import Any, overload

class Base:
    @overload
    def method(self) -> None: ...
    @overload
    def method(self, arg: int) -> int: ...
    @final
    def method(self, x=None):
        ...

Es ist ein Fehler, @final auf eine Nicht-Methodenfunktion anzuwenden.

Die Final Annotation

Der typing.Final Typqualifikator wird verwendet, um anzuzeigen, dass eine Variable oder ein Attribut nicht neu zugewiesen, neu definiert oder überschrieben werden sollte.

Syntax

Final kann in einer von mehreren Formen verwendet werden

  • Mit einem expliziten Typ unter Verwendung der Syntax Final[<Typ>]. Beispiel
    ID: Final[float] = 1
    
  • Ohne Typannotation. Beispiel
    ID: Final = 1
    

    Der Typüberprüfer sollte seine üblichen Typinferenzmechanismen anwenden, um den Typ von ID zu bestimmen (hier wahrscheinlich int). Beachten Sie, dass dies im Gegensatz zu generischen Klassen *nicht* dasselbe ist wie Final[Any].

  • In Klassenkörpern und Stub-Dateien können Sie die rechte Seite weglassen und einfach ID: Final[float] schreiben. Wenn die rechte Seite weggelassen wird, muss ein explizites Typargument für Final vorhanden sein.
  • Schließlich, wie self.id: Final = 1 (optional auch mit einem Typ in eckigen Klammern). Dies ist *nur* in __init__ Methoden erlaubt, damit das finale Instanzattribut nur einmal bei der Erstellung einer Instanz zugewiesen wird.

Semantik und Beispiele

Die beiden Hauptregeln für die Definition eines finalen Namens sind

  • Es kann *höchstens eine* finale Deklaration pro Modul oder Klasse für ein gegebenes Attribut geben. Es kann keine separaten Konstanten auf Klassen- und Instanzebene mit demselben Namen geben.
  • Es muss *genau eine* Zuweisung an einen finalen Namen geben.

Das bedeutet, ein Typüberprüfer sollte weitere Zuweisungen an finale Namen in typüberprüftem Code verhindern

from typing import Final

RATE: Final = 3000

class Base:
    DEFAULT_ID: Final = 0

RATE = 300  # Error: can't assign to final attribute
Base.DEFAULT_ID = 1  # Error: can't override a final attribute

Beachten Sie, dass ein Typüberprüfer Final-Deklarationen nicht innerhalb von Schleifen zulassen muss, da die Laufzeit in nachfolgenden Iterationen mehrere Zuweisungen an dieselbe Variable sehen würde.

Zusätzlich sollte ein Typüberprüfer verhindern, dass finale Attribute in einer Unterklasse überschrieben werden

from typing import Final

class Window:
    BORDER_WIDTH: Final = 2.5
    ...

class ListView(Window):
    BORDER_WIDTH = 3  # Error: can't override a final attribute

Ein finales Attribut, das in einem Klassenkörper ohne Initialisierer deklariert wurde, muss in der __init__ Methode initialisiert werden (außer in Stub-Dateien)

class ImmutablePoint:
    x: Final[int]
    y: Final[int]  # Error: final attribute without an initializer

    def __init__(self) -> None:
        self.x = 1  # Good

Typüberprüfer sollten ein finales Attribut, das in einem Klassenkörper initialisiert wird, als Klassenvariable inferieren. Variablen sollten nicht sowohl mit ClassVar als auch mit Final annotiert werden.

Final kann nur als äußerster Typ bei Zuweisungen oder Variablenannotationen verwendet werden. Die Verwendung an einer anderen Stelle ist ein Fehler. Insbesondere kann Final nicht in Annotationen für Funktionsargumente verwendet werden

x: List[Final[int]] = []  # Error!

def fun(x: Final[List[int]]) ->  None:  # Error!
    ...

Beachten Sie, dass die Deklaration eines Namens als final nur garantiert, dass der Name nicht an einen anderen Wert gebunden wird, aber den Wert nicht unveränderlich macht. Unveränderliche ABCs und Container können in Kombination mit Final verwendet werden, um die Änderung solcher Werte zu verhindern

x: Final = ['a', 'b']
x.append('c')  # OK

y: Final[Sequence[str]] = ['a', 'b']
y.append('x')  # Error: "Sequence[str]" has no attribute "append"
z: Final = ('a', 'b')  # Also works

Typüberprüfer sollten Verwendungen eines finalen Namens, der mit einem Literal initialisiert wurde, so behandeln, als ob er durch das Literal ersetzt worden wäre. Zum Beispiel sollte Folgendes erlaubt sein

from typing import NamedTuple, Final

X: Final = "x"
Y: Final = "y"
N = NamedTuple("N", [(X, int), (Y, int)])

Referenzimplementierung

Der mypy [1] Typüberprüfer unterstützt Final und final. Eine Referenzimplementierung der Laufzeitkomponente wird im Modul typing_extensions [2] bereitgestellt.

Abgelehnte/verschobene Ideen

Der Name Const wurde auch als Name für die Final Typannotation in Betracht gezogen. Stattdessen wurde der Name Final gewählt, da die Konzepte verwandt sind und es am besten schien, zwischen ihnen konsistent zu sein.

Wir haben erwogen, einen einzigen Namen Final anstelle der Einführung von final zu verwenden, aber @Final sah für uns einfach zu seltsam aus.

Eine verwandte Funktion zu finalen Klassen wären Scala-ähnliche versiegelte Klassen, bei denen eine Klasse nur von Klassen geerbt werden darf, die im selben Modul definiert sind. Versiegelte Klassen scheinen am nützlichsten in Kombination mit Pattern Matching zu sein, so dass es in unserem Fall die Komplexität nicht rechtfertigt. Dies könnte in Zukunft noch einmal überdacht werden.

Es wäre möglich, dass der @final Dekorator auf Klassen zur Laufzeit dynamisch das Unterklassifizieren verhindert. Nichts anderes in typing erzwingt Laufzeitprüfungen, daher wird final dies auch nicht tun. Ein Workaround, wenn sowohl Laufzeit- als auch statische Überprüfung gewünscht sind, ist die Verwendung dieses Idioms (möglicherweise in einem Support-Modul)

if typing.TYPE_CHECKING:
    from typing import final
else:
    from runtime_final import final

Referenzen


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

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