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