PEP 673 – Self 型別
- 作者:
- Pradeep Kumar Srinivasan <gohanpra at gmail.com>,James Hilton-Balfe <gobot1234yt at gmail.com>
- 發起人:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 討論至:
- Typing-SIG 郵件列表
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 主題:
- 型別標註
- 建立日期:
- 2021年11月10日
- Python 版本:
- 3.11
- 釋出歷史:
- 2021年11月17日
- 決議:
- Python-Dev 帖子
摘要
本 PEP 引入了一種簡單直觀的方法來註解返回其類例項的方法。這與 PEP 484 中指定的基於 TypeVar 的方法行為相同,但更簡潔、易於理解。
動機
一個常見的用例是編寫一個返回同一類例項的方法,通常透過返回 self 來實現。
class Shape:
def set_scale(self, scale: float):
self.scale = scale
return self
Shape().set_scale(0.5) # => should be Shape
一種表示返回型別的方法是將其指定為當前類,例如 Shape。使用該方法使型別檢查器按預期推斷型別 Shape。
class Shape:
def set_scale(self, scale: float) -> Shape:
self.scale = scale
return self
Shape().set_scale(0.5) # => Shape
然而,當我們在 Shape 的子類上呼叫 set_scale 時,型別檢查器仍然推斷返回型別為 Shape。這在以下所示情況下會產生問題,型別檢查器將返回錯誤,因為我們試圖使用基類中不存在的屬性或方法。
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
對於此類例項,目前的解決方法是定義一個以基類為邊界的 TypeVar,並將其用作 self 引數和返回型別的註解
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
不幸的是,這既冗長又不夠直觀。由於 self 通常不明確註解,上述解決方案不會立即想到,即使想到了,也很容易因忘記 TypeVar(bound="Shape") 上的邊界或 self 的註解而出錯。
這種困難意味著使用者常常放棄,要麼使用 Any 等回退型別,要麼完全省略型別註解,這兩者都會使程式碼不安全。
我們提出了一種更直觀、更簡潔的表達上述意圖的方式。我們引入了一個特殊形式 Self,它代表一個繫結到封裝類的型別變數。對於上述情況,使用者只需將返回型別註解為 Self
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
透過將返回型別註解為 Self,我們不再需要宣告一個帶有基類顯式邊界的 TypeVar。返回型別 Self 反映了函式返回 self 的事實,更容易理解。
如上例所示,型別檢查器將按預期正確推斷 Circle().set_scale(0.5) 的型別為 Circle。
使用統計
我們 分析了 流行的開源專案,發現上述模式的使用頻率約為 dict 或 Callable 等流行型別的 40%。例如,僅在 typeshed 中,此類“Self”型別使用了 523 次,而 dict 使用了 1286 次,Callable 使用了 1314 次(截至 2021 年 10 月)。這表明 Self 型別將非常常用,使用者將從上述更簡單的方法中受益匪淺。
規範
在方法簽名中使用
在方法簽名中使用的 Self 被視為繫結到該類的 TypeVar。
from typing import Self
class Shape:
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
被視為等效於
from typing import TypeVar
SelfShape = TypeVar("SelfShape", bound="Shape")
class Shape:
def set_scale(self: SelfShape, scale: float) -> SelfShape:
self.scale = scale
return self
這也適用於子類
class Circle(Shape):
def set_radius(self, radius: float) -> Self:
self.radius = radius
return self
其被視為等效於
SelfCircle = TypeVar("SelfCircle", bound="Circle")
class Circle(Shape):
def set_radius(self: SelfCircle, radius: float) -> SelfCircle:
self.radius = radius
return self
一種實現策略是簡單地在預處理步驟中將前者解糖為後者。如果一個方法在其簽名中使用 Self,則方法中 self 的型別將是 Self。在其他情況下,self 的型別將保持為封裝類。
在類方法簽名中使用
Self 型別註解對於返回其所操作的類例項的類方法也很有用。例如,以下程式碼片段中的 from_config 根據給定的 config 構建一個 Shape 物件。
class Shape:
def __init__(self, scale: float) -> None: ...
@classmethod
def from_config(cls, config: dict[str, float]) -> Shape:
return cls(config["scale"])
然而,這意味著 Circle.from_config(...) 被推斷為返回型別為 Shape 的值,而實際上它應該是 Circle
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`
目前的解決方法是不直觀且容易出錯的
Self = TypeVar("Self", bound="Shape")
class Shape:
@classmethod
def from_config(
cls: type[Self], config: dict[str, float]
) -> Self:
return cls(config["scale"])
我們建議直接使用 Self
from typing import Self
class Shape:
@classmethod
def from_config(cls, config: dict[str, float]) -> Self:
return cls(config["scale"])
這避免了複雜的 cls: type[Self] 註解和帶有 bound 的 TypeVar 宣告。同樣,後者程式碼的行為等同於前者程式碼。
在引數型別中使用
Self 的另一個用途是註解期望當前類例項的引數
Self = TypeVar("Self", bound="Shape")
class Shape:
def difference(self: Self, other: Self) -> float: ...
def apply(self: Self, f: Callable[[Self], None]) -> None: ...
我們建議直接使用 Self 來實現相同的行為
from typing import Self
class Shape:
def difference(self, other: Self) -> float: ...
def apply(self, f: Callable[[Self], None]) -> None: ...
請注意,指定 self: Self 是無害的,因此一些使用者可能會發現將其寫成如下形式更具可讀性
class Shape:
def difference(self: Self, other: Self) -> float: ...
在屬性註解中使用
Self 的另一個用途是註解屬性。一個例子是我們有一個 LinkedList,其元素必須是當前類的子類。
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"))
然而,將 next 屬性註解為 LinkedList[T] 允許使用子類進行無效構建
@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.
我們建議使用 next: Self | None 來表達此約束
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
上面的程式碼在語義上等同於將每個包含 Self 型別的屬性視為返回該型別的 property
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)
在泛型類中使用
Self 也可以用於泛型類方法中
class Container(Generic[T]):
value: T
def set_value(self, value: T) -> Self: ...
這等同於編寫
Self = TypeVar("Self", bound="Container[Any]")
class Container(Generic[T]):
value: T
def set_value(self: Self, value: T) -> Self: ...
其行為是保留方法被呼叫物件的型別引數。當在具體型別為 Container[int] 的物件上呼叫時,Self 繫結到 Container[int]。當在泛型型別為 Container[T] 的物件上呼叫時,Self 繫結到 Container[T]
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]
本 PEP 沒有指定方法 set_value 中 self.value 的確切型別。一些型別檢查器可能會選擇使用帶有 Self = TypeVar(“Self”, bound=Container[T]) 的類區域性型別變數來實現 Self 型別,這將推斷出一個精確的型別 T。然而,考慮到類區域性型別變數不是標準化的型別系統功能,推斷 Any 對於 self.value 也是可以接受的。我們將其留給型別檢查器決定。
請注意,我們拒絕使用帶有型別引數的 Self,例如 Self[int]。這是因為它會造成關於 self 引數型別的不明確性,並引入不必要的複雜性
class Container(Generic[T]):
def foo(
self, other: Self[int], other2: Self,
) -> Self[str]: # Rejected
...
在這種情況下,我們建議為 self 使用顯式型別
class Container(Generic[T]):
def foo(
self: Container[T],
other: Container[int],
other2: Container[T]
) -> Container[str]: ...
在協議中使用
Self 在協議中是有效的,類似於它在類中的使用
from typing import Protocol, Self
class ShapeProtocol(Protocol):
scale: float
def set_scale(self, scale: float) -> Self:
self.scale = scale
return self
被視為等效於
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
有關繫結到協議的 TypeVar 行為的詳細資訊,請參閱 PEP 544。
檢查類與協議的相容性:如果一個協議在方法或屬性註解中使用 Self,那麼如果一個類 Foo 的相應方法和屬性註解使用 Self 或 Foo 或 Foo 的任何子類,則該類被認為與協議相容。請參閱下面的示例
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)
Self 的有效位置
Self 註解僅在類上下文中有效,並且始終指代封裝類。在涉及巢狀類的上下文中,Self 將始終指代最內層的類。
以下 Self 用法被接受
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)
以下 Self 用法被拒絕。
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
我們拒絕包含 Self 的類型別名。在類定義之外支援 Self 可能需要型別檢查器進行大量特殊處理。鑑於在類定義之外使用 Self 也與本 PEP 的其餘部分相悖,我們認為別名帶來的額外便利不值得
TupleSelf = Tuple[Self, Self] # Rejected
class Alias:
def return_tuple(self) -> TupleSelf: # Rejected
return (self, self)
請注意,我們拒絕在靜態方法中使用 Self。Self 沒有太多價值,因為沒有 self 或 cls 可返回。唯一可能的用例是返回引數本身或從作為引數傳入的容器中返回某個元素。這些似乎不值得增加額外的複雜性。
class Base:
@staticmethod
def make() -> Self: # Rejected
...
@staticmethod
def return_parameter(foo: Self) -> Self: # Rejected
...
同樣,我們拒絕在元類中使用 Self。本 PEP 中的 Self 始終指代相同的型別(即 self 的型別)。但在元類中,它必須在不同的方法簽名中指代不同的型別。例如,在 __mul__ 中,返回型別中的 Self 將指代實現類 Foo,而不是封裝類 MyMetaclass。但是,在 __new__ 中,返回型別中的 Self 將指代封裝類 MyMetaclass。為了避免混淆,我們拒絕這種邊緣情況。
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): ...
執行時行為
由於 Self 不可下標,我們建議採用類似於 typing.NoReturn 的實現。
@_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")
被拒絕的替代方案
允許型別檢查器推斷返回型別
一個提議是讓 Self 型別隱式化,並讓型別檢查器從方法體中推斷出返回型別必須與 self 引數的型別相同
class Shape:
def set_scale(self, scale: float):
self.scale = scale
return self # Type checker infers that we are returning self
我們拒絕這種做法,因為“顯式優於隱式”。除此之外,上述方法對於型別存根將失敗,因為它們沒有方法體可供分析。
參考實現
Mypy:Mypy 中的概念驗證實現。
Pyright:v1.1.184
Self 的執行時實現:PR。
資源
關於 Python 中 Self 型別的類似討論始於 Mypy 大約 2016 年:Mypy issue #1212 - SelfType 或另一種拼寫“self 型別”的方式。然而,最終在那裡採取的方法是我們的“之前”示例中顯示的有界 TypeVar 方法。討論此問題的其他議題包括 Mypy issue #2354 - 泛型類中的 Self 型別。
- Pradeep 在 PyCon Typing Summit 2021 上提出了一個具體提案
- 錄音演講,幻燈片。
James 在 typing-sig 上獨立提出了該提案:Typing-sig 執行緒。
其他語言也有類似的方式來表達封裝類的型別
- TypeScript 有
this型別(TypeScript 文件) - Rust 有
Self型別(Rust 文件)
感謝以下人員對本 PEP 的反饋
Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, Tuomas Suutari, Eric Traut, Alex Waygood, Shannon Zhu, 和 Никита Соболев
版權
本文件置於公共領域或 CC0-1.0-Universal 許可證下,以更寬鬆者為準。
來源:https://github.com/python/peps/blob/main/peps/pep-0673.rst