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
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.
- TypeScript hat den
this-Typ (TypeScript Docs). - Rust hat den
Self-Typ (Rust Docs).
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 Никита Соболев
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0673.rst
Zuletzt geändert: 2024-06-11 22:12:09 GMT