PEP 673 – 自我类型
- 作者:
- 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
。但是,鉴于类局部类型变量不是标准化的类型系统特性,因此对于 self.value
推断 Any
也是可以接受的。我们将其留给类型检查器决定。
请注意,我们拒绝在带有类型参数的 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
有关与协议绑定的 TypeVars 的行为的详细信息,请参阅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
可能需要大量的特殊处理。鉴于它也违反了 PEP 在类定义之外使用Self
的其余部分,我们认为别名的额外便利性不值得。
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
类型的类似讨论始于 2016 年左右的 Mypy:Mypy issue #1212 - SelfType 或另一种拼写“self 的类型”。但是,最终采用的方法是我们“之前”示例中所示的有界TypeVar
方法。讨论此问题的其他问题包括Mypy issue #2354 - 泛型类中的 Self 类型。
- Pradeep 在 2021 年 PyCon Typing 峰会上提出了一个具体的提议。
- 录制谈话,幻灯片。
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
上次修改时间:2024-06-11 22:12:09 GMT