Python 如何键入注释 returns 自身的方法?

Python how to type anotate a method that returns self?

假设我有一个 class 实现方法链:

from __future__ import annotations

class M:
    def set_width(self, width: int)->M:
        self.width = width
        return self

    def set_height(self, height: int)->M:
        self.height = height
        return self

我可以这样使用它:

box = M().set_width(5).set_height(10)

这行得通,但是如果我有一个 subclass M3D:

class M3D(M):
    def set_depth(self, depth: int) -> M3D:
        self.depth = depth
        return self

现在我不能这样做了:

cube = M3D().set_width(2).set_height(3).set_depth(5)

我在 mypy 中得到以下错误:

_test_typeanotations.py:21: error: "M" has no attribute "set_depth"; maybe "set_width"

因为 set_width() returns 一个 M 没有方法 set_depth。我看到过为每个 subclass 重写 set_width()set_height() 以指定正确类型的建议,但这会为每个方法编写大量代码。必须有更简单的方法。

这也与特殊方法相关,例如 __enter__ 传统上 returns self,因此最好有一种方法来指定它而无需提及它在 subclasses.

这是任何使用继承的语言中的经典问题。不同语言的处理方式不同:

  • 在 C++ 中,您将在调用 set_depth
  • 之前转换 set_height 的结果
  • 在 Java 中,您可以使用与 C++ 相同的强制转换,或者让 IDE 生成一堆覆盖并且仅在覆盖方法中手动更改类型。

Python 是动态类型语言,所以没有cast指令。所以你剩下 3 种可能的方式:

  • 勇敢的方法:覆盖所有相关方法来调用基方法并在return注释中声明新类型
  • 我不关心方式:注释控件只给出警告。如您所知,这条线没问题,您可以忽略警告
  • 别打扰我方式:注释在Python中是可选的,通常可以通过特殊注释暂停注释控制。这里知道没有问题,所以你可以放心地暂停那个指令或者那个方法的类型控制。

以下仅代表本人意见

如果可能的话,我会避免不要打扰的方式,因为如果您将在代码中留下警告,您将不得不在每次更改后进行控制(如果有)是一个新的警告。

我不会为了摆脱警告而覆盖方法。毕竟 Python 是一种动态类型的语言,甚至允许 duck typing。如果我知道代码是正确的,我会避免添加无用的代码(DRY 和 KISS 原则)

所以我只是假设暂停注释控件的注释是出于某种原因发明并使用它们(我称之为不要在这里打扰我)。

经过大量研究和实验,我找到了一种在 mypy 中有效的方法,尽管 Pycham 有时仍然会猜错类型。

诀窍是让 self 成为一个类型 var:

from __future__ import annotations

import asyncio
from typing import TypeVar

T = TypeVar('T')


class M:
    def set_width(self: T, width: int)->T:
        self.width = width
        return self

    def set_height(self: T, height: int)->T:
        self.height = height
        return self

    def copy(self)->M:
        return M().set_width(self.width).set_height(self.height)


class M3D(M):
    def set_depth(self: T, depth: int) -> T:
        self.depth = depth
        return self

box = M().set_width(5).set_height(10) # box has correct type
cube = M3D().set_width(2).set_height(3).set_depth(5) # cube has correct type
attemptToTreatBoxAsCube = M3D().copy().set_depth(4) # Mypy gets angry as expected

最后一行在 mypy 中特别有效,但 pycharm 有时仍会自动完成 set_depth 即使 .copy() 实际上 returns 和 M 即使被调用M3D.