Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 是一份历史文档:请参阅 Selftyping.Self 获取最新的规范和文档。规范的类型规范维护在 类型规范站点;运行时类型行为在 CPython 文档中进行了描述。

×

请参阅 类型规范更新流程,了解如何提议更改类型规范。

摘要

此 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,正如预期的那样。

使用统计

我们 分析 了流行的开源项目,发现像上面这样的模式的使用频率大约是流行类型(如 dictCallable)的 40%。例如,仅在 typeshed 中,此类“Self”类型就被使用了 523 次,而 dict 被使用了 1286 次,Callable 被使用了 1314 次 截至 2021 年 10 月。这表明 Self 类型将被频繁使用,用户将从上述更简单的方法中获益良多。

Python 类型用户也经常请求此功能,无论是在 提案文档 还是在 GitHub 上。

规范

在方法签名中使用

在方法签名中使用的 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] 注释和带有 boundTypeVar 声明。再次强调,后者的代码行为等同于前者。

在参数类型中使用

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_valueself.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被认为与该协议兼容,如果其对应的方法和属性注解使用SelfFooFoo的任何子类。请参阅下面的示例。

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)

请注意,我们拒绝在静态方法中使用SelfSelf没有增加太多价值,因为没有selfcls可以返回。唯一可能的使用案例是返回参数本身或作为参数传入的容器中的某些元素。这些似乎不值得增加复杂性。

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 线程

其他语言也有类似的方法来表达封闭类的类型。

感谢以下人员对 PEP 的反馈。

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


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

上次修改时间:2024-06-11 22:12:09 GMT