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

Python 增强提案

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 是一份历史文档:有关最新的规范和文档,请参阅 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。然而,考虑到类局部类型变量不是标准化的类型系统功能,推断 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 的相应方法和属性注解使用 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 可能需要类型检查器进行大量特殊处理。鉴于在类定义之外使用 Self 也与本 PEP 的其余部分相悖,我们认为别名带来的额外便利不值得

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 类型的类似讨论始于 Mypy 大约 2016 年:Mypy issue #1212 - SelfType 或另一种拼写“self 类型”的方式。然而,最终在那里采取的方法是我们的“之前”示例中显示的有界 TypeVar 方法。讨论此问题的其他议题包括 Mypy issue #2354 - 泛型类中的 Self 类型。

Pradeep 在 PyCon Typing Summit 2021 上提出了一个具体提案
录音演讲幻灯片

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