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

Python Enhancement Proposals

PEP 673 – Self Type

Autor:
Pradeep Kumar Srinivasan <gohanpra at gmail.com>, James Hilton-Balfe <gobot1234yt at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Typing-SIG list
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
10. Nov. 2021
Python-Version:
3.11
Post-History:
17. Nov. 2021
Resolution:
Python-Dev thread

Inhaltsverzeichnis

Wichtig

Diese PEP ist ein historisches Dokument: siehe Self und typing.Self für aktuelle Spezifikationen und Dokumentationen. Kanonische Tippspezifikationen werden auf der Website für Tippspezifikationen gepflegt; Laufzeit-Tippverhalten 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 eine einfache und intuitive Möglichkeit ein, Methoden zu annotieren, die eine Instanz ihrer Klasse zurückgeben. Dies verhält sich genauso wie der auf TypeVar basierende Ansatz, der in PEP 484 spezifiziert ist, ist aber prägnanter und leichter nachvollziehbar.

Motivation

Ein häufiger Anwendungsfall ist das Schreiben einer Methode, die eine Instanz derselben Klasse zurückgibt, normalerweise durch Rückgabe von self.

class Shape:
    def set_scale(self, scale: float):
        self.scale = scale
        return self

Shape().set_scale(0.5)  # => should be Shape

Eine Möglichkeit, den Rückgabetyp zu bezeichnen, besteht darin, ihn als die aktuelle Klasse anzugeben, z. B. Shape. Die Verwendung der Methode bewirkt, dass der Typüberprüfer den Typ Shape ableitet, wie erwartet.

class Shape:
    def set_scale(self, scale: float) -> Shape:
        self.scale = scale
        return self

Shape().set_scale(0.5)  # => Shape

Wenn wir jedoch set_scale auf einer Unterklasse von Shape aufrufen, leitet der Typüberprüfer immer noch den Rückgabetyp als Shape ab. Dies ist problematisch in Situationen wie der unten gezeigten, in der der Typüberprüfer einen Fehler zurückgibt, da wir versuchen, Attribute oder Methoden zu verwenden, die nicht in der Basisklasse vorhanden sind.

class Circle(Shape):
    def set_radius(self, r: float) -> Circle:
        self.radius = r
        return self

Circle().set_scale(0.5)  # *Shape*, not Circle
Circle().set_scale(0.5).set_radius(2.7)
# => Error: Shape has no attribute set_radius

Die derzeitige Umgehungslösung für solche Fälle besteht darin, ein TypeVar mit der Basisklasse als Bound zu definieren und es als Annotation für den self-Parameter und den Rückgabetyp zu verwenden.

from typing import TypeVar

TShape = TypeVar("TShape", bound="Shape")

class Shape:
    def set_scale(self: TShape, scale: float) -> TShape:
        self.scale = scale
        return self


class Circle(Shape):
    def set_radius(self, radius: float) -> Circle:
        self.radius = radius
        return self

Circle().set_scale(0.5).set_radius(2.7)  # => Circle

Leider ist dies umständlich und unintuitiv. Da self normalerweise nicht explizit annotiert wird, kommt die obige Lösung nicht sofort in den Sinn, und selbst wenn, ist es sehr leicht, Fehler zu machen, indem man entweder den Bound auf dem TypeVar(bound="Shape") oder die Annotation für self vergisst.

Diese Schwierigkeit führt dazu, dass Benutzer oft aufgeben und entweder Fallback-Typen wie Any verwenden oder die Typannotation ganz weglassen, was beides den Code weniger sicher macht.

Wir schlagen eine intuitivere und prägnantere Methode vor, um die obige Absicht auszudrücken. Wir führen eine spezielle Form Self ein, die für eine Typvariable steht, die an die umschließende Klasse gebunden ist. Für Situationen wie die oben beschriebene muss der Benutzer nur den Rückgabetyp als Self annotieren.

from typing import Self

class Shape:
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self


class Circle(Shape):
    def set_radius(self, radius: float) -> Self:
        self.radius = radius
        return self

Durch die Annotation des Rückgabetyps als Self müssen wir kein TypeVar mit einem expliziten Bound auf der Basisklasse deklarieren. Der Rückgabetyp Self spiegelt die Tatsache wider, dass die Funktion self zurückgibt und leichter zu verstehen ist.

Wie im obigen Beispiel wird der Typüberprüfer den Typ von Circle().set_scale(0.5) korrekt als Circle ableiten, wie erwartet.

Nutzungsstatistiken

Wir haben beliebte Open-Source-Projekte analysiert und festgestellt, dass Muster wie die oben genannten etwa **40 %** so oft verwendet wurden wie beliebte Typen wie dict oder Callable. Beispielsweise werden in typeshed allein solche „Self“-Typen im Oktober 2021 523 Mal verwendet, verglichen mit 1286 Verwendungen von dict und 1314 Verwendungen von Callable (Stand Oktober 2021). Dies deutet darauf hin, dass ein Self-Typ recht häufig verwendet wird und die Benutzer von dem einfacheren Ansatz oben stark profitieren werden.

Benutzer von Python-Typen haben diese Funktion auch häufig angefordert, sowohl in der Vorschlagsdokumentation als auch auf GitHub.

Spezifikation

Verwendung in Methodensignaturen

Self, das in der Signatur einer Methode verwendet wird, wird so behandelt, als wäre es eine an die Klasse gebundene TypeVar.

from typing import Self

class Shape:
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

wird äquivalent behandelt zu

from typing import TypeVar

SelfShape = TypeVar("SelfShape", bound="Shape")

class Shape:
    def set_scale(self: SelfShape, scale: float) -> SelfShape:
        self.scale = scale
        return self

Dies funktioniert auch für eine Unterklasse

class Circle(Shape):
    def set_radius(self, radius: float) -> Self:
        self.radius = radius
        return self

was äquivalent behandelt wird zu

SelfCircle = TypeVar("SelfCircle", bound="Circle")

class Circle(Shape):
    def set_radius(self: SelfCircle, radius: float) -> SelfCircle:
        self.radius = radius
        return self

Eine Implementierungsstrategie ist es, die erstere in einem Vorverarbeitungsschritt einfach in letztere zu zerlegen. Wenn eine Methode Self in ihrer Signatur verwendet, ist der Typ von self innerhalb einer Methode Self. In anderen Fällen bleibt der Typ von self die umschließende Klasse.

Verwendung in Classmethod-Signaturen

Die Self-Typannotation ist auch nützlich für Klassenmethoden, die eine Instanz der Klasse zurückgeben, mit der sie arbeiten. Zum Beispiel erstellt from_config im folgenden Snippet ein Shape-Objekt aus einer gegebenen config.

class Shape:
    def __init__(self, scale: float) -> None: ...

    @classmethod
    def from_config(cls, config: dict[str, float]) -> Shape:
        return cls(config["scale"])

Dies bedeutet jedoch, dass Circle.from_config(...) so abgeleitet wird, dass ein Wert vom Typ Shape zurückgegeben wird, wenn er tatsächlich Circle sein sollte.

class Circle(Shape):
    def circumference(self) -> float: ...

shape = Shape.from_config({"scale": 7.0})
# => Shape

circle = Circle.from_config({"scale": 7.0})
# => *Shape*, not Circle

circle.circumference()
# Error: `Shape` has no attribute `circumference`

Die aktuelle Umgehungslösung hierfür ist unintuitiv und fehleranfällig.

Self = TypeVar("Self", bound="Shape")

class Shape:
    @classmethod
    def from_config(
        cls: type[Self], config: dict[str, float]
    ) -> Self:
        return cls(config["scale"])

Wir schlagen vor, Self direkt zu verwenden.

from typing import Self

class Shape:
    @classmethod
    def from_config(cls, config: dict[str, float]) -> Self:
        return cls(config["scale"])

Dies vermeidet die komplizierte cls: type[Self]-Annotation und die TypeVar-Deklaration mit einem bound. Auch hier verhält sich der letztere Code äquivalent zum ersteren.

Verwendung in Parametertypen

Eine weitere Verwendung von Self ist die Annotation von Parametern, die Instanzen der aktuellen Klasse erwarten.

Self = TypeVar("Self", bound="Shape")

class Shape:
    def difference(self: Self, other: Self) -> float: ...

    def apply(self: Self, f: Callable[[Self], None]) -> None: ...

Wir schlagen vor, Self direkt zu verwenden, um das gleiche Verhalten zu erzielen.

from typing import Self

class Shape:
    def difference(self, other: Self) -> float: ...

    def apply(self, f: Callable[[Self], None]) -> None: ...

Beachten Sie, dass die Angabe von self: Self harmlos ist, sodass es für einige Benutzer möglicherweise lesbarer ist, das obige als Folgendes zu schreiben:

class Shape:
    def difference(self: Self, other: Self) -> float: ...

Verwendung in Attribut-Annotationen

Eine weitere Verwendung von Self ist die Annotation von Attributen. Ein Beispiel ist eine LinkedList, deren Elemente Unterklassen der aktuellen Klasse sein müssen.

from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar("T")

@dataclass
class LinkedList(Generic[T]):
    value: T
    next: LinkedList[T] | None = None

# OK
LinkedList[int](value=1, next=LinkedList[int](value=2))
# Not OK
LinkedList[int](value=1, next=LinkedList[str](value="hello"))

Die Annotation des next-Attributs als LinkedList[T] erlaubt jedoch ungültige Konstruktionen mit Unterklassen.

@dataclass
class OrdinalLinkedList(LinkedList[int]):
    def ordinal_value(self) -> str:
        return as_ordinal(self.value)

# Should not be OK because LinkedList[int] is not a subclass of
# OrdinalLinkedList, # but the type checker allows it.
xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))

if xs.next:
    print(xs.next.ordinal_value())  # Runtime Error.

Wir schlagen vor, diese Einschränkung mit next: Self | None auszudrücken.

from typing import Self

@dataclass
class LinkedList(Generic[T]):
    value: T
    next: Self | None = None

@dataclass
class OrdinalLinkedList(LinkedList[int]):
    def ordinal_value(self) -> str:
        return as_ordinal(self.value)

xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))
# Type error: Expected OrdinalLinkedList, got LinkedList[int].

if xs.next is not None:
    xs.next = OrdinalLinkedList(value=3, next=None)  # OK
    xs.next = LinkedList[int](value=3, next=None)  # Not OK

Der obige Code ist semantisch äquivalent zur Behandlung jedes Attributs, das einen Self-Typ enthält, als property, das diesen Typ zurückgibt.

from dataclasses import dataclass
from typing import Any, Generic, TypeVar

T = TypeVar("T")
Self = TypeVar("Self", bound="LinkedList")


class LinkedList(Generic[T]):
    value: T

    @property
    def next(self: Self) -> Self | None:
        return self._next

    @next.setter
    def next(self: Self, next: Self | None) -> None:
        self._next = next

class OrdinalLinkedList(LinkedList[int]):
    def ordinal_value(self) -> str:
        return str(self.value)

Verwendung in generischen Klassen

Self kann auch in generischen Klassenmethoden verwendet werden.

class Container(Generic[T]):
    value: T
    def set_value(self, value: T) -> Self: ...

Dies entspricht dem Schreiben von

Self = TypeVar("Self", bound="Container[Any]")

class Container(Generic[T]):
    value: T
    def set_value(self: Self, value: T) -> Self: ...

Das Verhalten besteht darin, das Typargument des Objekts beizubehalten, auf dem die Methode aufgerufen wurde. Wenn sie für ein Objekt mit dem konkreten Typ Container[int] aufgerufen wird, wird Self an Container[int] gebunden. Wenn sie für ein Objekt vom generischen Typ Container[T] aufgerufen wird, wird Self an Container[T] gebunden.

def object_with_concrete_type() -> None:
    int_container: Container[int]
    str_container: Container[str]
    reveal_type(int_container.set_value(42))  # => Container[int]
    reveal_type(str_container.set_value("hello"))  # => Container[str]

def object_with_generic_type(
    container: Container[T], value: T,
) -> Container[T]:
    return container.set_value(value)  # => Container[T]

Die PEP spezifiziert nicht den genauen Typ von self.value innerhalb der Methode set_value. Einige Typüberprüfer können sich dafür entscheiden, Self-Typen mithilfe von klassenlokalen Typvariablen mit Self = TypeVar(“Self”, bound=Container[T]) zu implementieren, was einen präzisen Typ T ableitet. Da klassenlokale Typvariablen jedoch keine standardisierte Funktion des Typsystems sind, ist es auch akzeptabel, Any für self.value abzuleiten. Dies überlassen wir dem Typüberprüfer.

Beachten Sie, dass wir die Verwendung von Self mit Typargumenten, wie z. B. Self[int], ablehnen. Dies liegt daran, dass es Mehrdeutigkeiten über den Typ des self-Parameters schafft und unnötige Komplexität einführt.

class Container(Generic[T]):
    def foo(
        self, other: Self[int], other2: Self,
    ) -> Self[str]:  # Rejected
        ...

In solchen Fällen empfehlen wir die Verwendung eines expliziten Typs für self.

class Container(Generic[T]):
    def foo(
        self: Container[T],
        other: Container[int],
        other2: Container[T]
    ) -> Container[str]: ...

Verwendung in Protokollen

Self ist innerhalb von Protokollen gültig, ähnlich wie seine Verwendung in Klassen.

from typing import Protocol, Self

class ShapeProtocol(Protocol):
    scale: float

    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

wird äquivalent behandelt zu

from typing import TypeVar

SelfShape = TypeVar("SelfShape", bound="ShapeProtocol")

class ShapeProtocol(Protocol):
    scale: float

    def set_scale(self: SelfShape, scale: float) -> SelfShape:
        self.scale = scale
        return self

Weitere Einzelheiten zum Verhalten von Typvariablen, die an Protokolle gebunden sind, finden Sie in PEP 544.

Überprüfung einer Klasse auf Kompatibilität mit einem Protokoll: Wenn ein Protokoll Self in Methoden- oder Attributannotationen verwendet, gilt eine Klasse Foo als kompatibel mit dem Protokoll, wenn ihre entsprechenden Methoden und Attributannotationen entweder Self oder Foo oder eine der Unterklassen von Foo verwenden. Siehe Beispiele unten.

from typing import Protocol

class ShapeProtocol(Protocol):
    def set_scale(self, scale: float) -> Self: ...

class ReturnSelf:
    scale: float = 1.0

    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

class ReturnConcreteShape:
    scale: float = 1.0

    def set_scale(self, scale: float) -> ReturnConcreteShape:
        self.scale = scale
        return self

class BadReturnType:
    scale: float = 1.0

    def set_scale(self, scale: float) -> int:
        self.scale = scale
        return 42

class ReturnDifferentClass:
    scale: float = 1.0

    def set_scale(self, scale: float) -> ReturnConcreteShape:
        return ReturnConcreteShape(...)

def accepts_shape(shape: ShapeProtocol) -> None:
    y = shape.set_scale(0.5)
    reveal_type(y)

def main() -> None:
    return_self_shape: ReturnSelf
    return_concrete_shape: ReturnConcreteShape
    bad_return_type: BadReturnType
    return_different_class: ReturnDifferentClass

    accepts_shape(return_self_shape)  # OK
    accepts_shape(return_concrete_shape)  # OK
    accepts_shape(bad_return_type)  # Not OK
    # Not OK because it returns a non-subclass.
    accepts_shape(return_different_class)

Gültige Orte für Self

Eine Self-Annotation ist nur in Klassenkontexten gültig und bezieht sich immer auf die umschließende Klasse. In Kontexten mit verschachtelten Klassen bezieht sich Self immer auf die innerste Klasse.

Die folgenden Verwendungen von Self werden akzeptiert.

class ReturnsSelf:
    def foo(self) -> Self: ... # Accepted

    @classmethod
    def bar(cls) -> Self:  # Accepted
        return cls()

    def __new__(cls, value: int) -> Self: ...  # Accepted

    def explicitly_use_self(self: Self) -> Self: ...  # Accepted

    # Accepted (Self can be nested within other types)
    def returns_list(self) -> list[Self]: ...

    # Accepted (Self can be nested within other types)
    @classmethod
    def return_cls(cls) -> type[Self]:
        return cls

class Child(ReturnsSelf):
    # Accepted (we can override a method that uses Self annotations)
    def foo(self) -> Self: ...

class TakesSelf:
    def foo(self, other: Self) -> bool: ...  # Accepted

class Recursive:
    # Accepted (treated as an @property returning ``Self | None``)
    next: Self | None

class CallableAttribute:
    def foo(self) -> int: ...

    # Accepted (treated as an @property returning the Callable type)
    bar: Callable[[Self], int] = foo

class HasNestedFunction:
    x: int = 42

    def foo(self) -> None:

        # Accepted (Self is bound to HasNestedFunction).
        def nested(z: int, inner_self: Self) -> Self:
            print(z)
            print(inner_self.x)
            return inner_self

        nested(42, self)  # OK


class Outer:
    class Inner:
        def foo(self) -> Self: ...  # Accepted (Self is bound to Inner)

Die folgenden Verwendungen von Self werden abgelehnt.

def foo(bar: Self) -> Self: ...  # Rejected (not within a class)

bar: Self  # Rejected (not within a class)

class Foo:
    # Rejected (Self is treated as unknown).
    def has_existing_self_annotation(self: T) -> Self: ...

class Foo:
    def return_concrete_type(self) -> Self:
        return Foo()  # Rejected (see FooChild below for rationale)

class FooChild(Foo):
    child_value: int = 42

    def child_method(self) -> None:
        # At runtime, this would be Foo, not FooChild.
        y = self.return_concrete_type()

        y.child_value
        # Runtime error: Foo has no attribute child_value

class Bar(Generic[T]):
    def bar(self) -> T: ...

class Baz(Bar[Self]): ...  # Rejected

Wir lehnen Aliasse für Typen ab, die Self enthalten. Die Unterstützung von Self außerhalb von Klassendefinitionen kann in Typüberprüfern eine Menge von Spezialbehandlungen erfordern. Da es auch gegen den Rest der PEP verstößt, Self außerhalb einer Klassendefinition zu verwenden, glauben wir, dass der zusätzliche Komfort von Aliasen es nicht wert ist.

TupleSelf = Tuple[Self, Self]  # Rejected

class Alias:
    def return_tuple(self) -> TupleSelf:  # Rejected
        return (self, self)

Beachten Sie, dass wir Self in statischen Methoden ablehnen. Self fügt nicht viel Wert hinzu, da es kein self oder cls gibt, das zurückgegeben werden könnte. Die einzig möglichen Anwendungsfälle wären die Rückgabe eines Parameters selbst oder eines Elements aus einem Container, der als Parameter übergeben wird. Diese scheinen die zusätzliche Komplexität nicht wert zu sein.

class Base:
    @staticmethod
    def make() -> Self:  # Rejected
        ...

    @staticmethod
    def return_parameter(foo: Self) -> Self:  # Rejected
        ...

Ebenso lehnen wir Self in Metaklassen ab. Self bezieht sich in dieser PEP konsistent auf denselben Typ (den von self). Aber in Metaklassen müsste es sich in verschiedenen Methodensignaturen auf unterschiedliche Typen beziehen. Zum Beispiel würde sich in __mul__ Self im Rückgabetyp auf die implementierende Klasse Foo beziehen, nicht auf die umschließende Klasse MyMetaclass. Aber in __new__ würde sich Self im Rückgabetyp auf die umschließende Klasse MyMetaclass beziehen. Um Verwirrung zu vermeiden, lehnen wir diesen Sonderfall ab.

class MyMetaclass(type):
    def __new__(cls, *args: Any) -> Self:  # Rejected
        return super().__new__(cls, *args)

    def __mul__(cls, count: int) -> list[Self]:  # Rejected
        return [cls()] * count

class Foo(metaclass=MyMetaclass): ...

Laufzeitverhalten

Da Self nicht subscriptfähig ist, schlagen wir eine Implementierung vor, die typing.NoReturn ähnelt.

@_SpecialForm
def Self(self, params):
    """Used to spell the type of "self" in classes.

    Example::

      from typing import Self

      class ReturnsSelf:
          def parse(self, data: bytes) -> Self:
              ...
              return self

    """
    raise TypeError(f"{self} is not subscriptable")

Abgelehnte Alternativen

Erlaube dem Typüberprüfer, den Rückgabetyp abzuleiten

Ein Vorschlag ist, den Self-Typ implizit zu lassen und den Typüberprüfer aus dem Methodenrumpf ableiten zu lassen, dass der Rückgabetyp derselbe sein muss wie der Typ des self-Parameters.

class Shape:
    def set_scale(self, scale: float):
        self.scale = scale
        return self  # Type checker infers that we are returning self

Wir lehnen dies ab, da explizit besser als implizit ist. Darüber hinaus wird der obige Ansatz für Typ-Stubs fehlschlagen, die keine Methodenrümpfe zur Analyse haben.

Referenzimplementierungen

Mypy: Proof-of-Concept-Implementierung in Mypy.

Pyright: v1.1.184

Laufzeitimplementierung von Self: PR.

Ressourcen

Ähnliche Diskussionen über einen Self-Typ in Python begannen in Mypy um 2016: Mypy Issue #1212 - SelfType oder eine andere Möglichkeit, „Typ von self“ zu schreiben. Der dort schließlich eingeschlagene Ansatz war jedoch der gebundene TypeVar-Ansatz, der in unseren „Vorher“-Beispielen gezeigt wird. Andere Issues, die dies diskutieren, sind Mypy Issue #2354 - Self types in generic classes.

Pradeep machte einen konkreten Vorschlag auf dem PyCon Typing Summit 2021.
Aufgenommener Vortrag, Folien.

James brachte den Vorschlag unabhängig auf typing-sig ein: Typing-sig Thread.

Andere Sprachen haben ähnliche Möglichkeiten, den Typ der umschließenden Klasse auszudrücken.

Dank der folgenden Personen für ihr Feedback zur PEP.

Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, Tuomas Suutari, Eric Traut, Alex Waygood, Shannon Zhu und Никита Соболев


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

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