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