通过子类化改变 Python 的类型参数

Mic*_*ert 7 python generics typing subclass mypy

Python 的类型系统允许在类中使用泛型:

class A(Generic[T]):
    def get_next(self) -> T
Run Code Online (Sandbox Code Playgroud)

这非常方便。然而,即使在 3.11 中使用 Self 类型,我也找不到T在不指定类名的情况下更改类型参数 (the ) 的方法。以下是 PEP 673 中的推荐用法: 自类型:https ://peps.python.org/pep-0673/a

class Container(Generic[T]):
    def foo(
        self: Container[T],
    ) -> Container[str]:
        # maybe implementing something like:
        return self.__class__([str(x) for x in self])
Run Code Online (Sandbox Code Playgroud)

问题是如果我想子类化容器:

class SuperContainer(Container[T]):
    def time_travel(self): ...
Run Code Online (Sandbox Code Playgroud)

然后,如果我有一个 SuperContainer 的实例并在其上调用 foo ,则输入将会错误,并认为它是一个 Container 而不是 SuperContainer。

sc = SuperContainer([1, 2, 3])
sc2 = sc.foo()
reveal_type(sc2)  # mypy: Container[str]
sc2.time_travel()  # typing error: only SuperContainers can time-travel
isinstance(sc2, SuperContainer)  # True
Run Code Online (Sandbox Code Playgroud)

是否有一种可接受的方法允许程序更改超类中的类型参数以保留子类的类型?

Hac*_*ck5 5

为了解决这个问题,您需要第二个泛型类型参数,来表示foo

\n
SelfStr = TypeVar("SelfStr", bound="Container[str, Any]", covariant=True)\n
Run Code Online (Sandbox Code Playgroud)\n

Any问题。我们稍后会看到。

\n

到目前为止,一切都很好。让我们定义Container

\n
class Container(Generic[T, SelfStr]):\n    def __init__(self, contents: list[T]):\n        self._contents = contents\n\n    def __iter__(self):\n        return iter(self._contents)\n\n    def foo(self) -> SelfStr:\n        reveal_type(type(self))\n        # Mypy is wrong here: it thinks that type(self) is already annotated, but in fact the type parameters are erased.\n        return type(self)([str(x) for x in self])  # type: ignore\n\n    def __repr__(self):\n        return type(self).__name__ + "(" + repr(self._contents) + ")"\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,我们必须忽略 中的类型foo。这是因为 mypy 错误地推断了类型type(self)。它认为type(self)返回Container[...](或子类),但实际上它返回Container(或子类)。当我们开始运行这段代码时,您就会看到这一点。

\n

接下来,我们需要某种方法来创建容器。我们希望类型看起来像Container[T, Container[str, Container[str, ...]]].

\n

在类声明的第一行中,我们将类的第二个类型参数设置为 a SelfStr,即它本身Container[str, Any]。这意味着 的定义SelfStr应该限于,因此我们应该得到我们想要的Container[str, SelfStr]上限。Container[str, Container[str, ...]]这是有效的:它只允许我们的递归类型(或子类)Any. 不幸的是, mypy 不会对递归泛型类型进行推理, reporting test.Container[builtins.int, <nothing>],所以我们必须做繁重的工作。是时候来一些\xe2\x9c\xa8 魔法 \xe2\x9c\xa8 了

\n
_ContainerStr: TypeAlias = Container[str, "_ContainerStr"]\nContainerComplete: TypeAlias = Container[T, _ContainerStr]\n
Run Code Online (Sandbox Code Playgroud)\n

别名_ContainerStr将为我们提供签名的递归部分。然后我们公开ContainerComplete,我们可以将其用作构造函数,例如:

\n
ContainerComplete[int]([1,2,3])\n
Run Code Online (Sandbox Code Playgroud)\n

惊人的!但是子类呢?我们只需要为我们的子类再次做同样的事情:

\n
class SuperContainer(Container[T, SelfStr]):\n    def time_travel(self):\n        return "magic"\n_SuperContainerStr: TypeAlias = SuperContainer[str, "_SuperContainerStr"]\nSuperContainerComplete: TypeAlias = SuperContainer[T, _SuperContainerStr]\n
Run Code Online (Sandbox Code Playgroud)\n

全做完了!现在我们来演示一下:

\n
sc = SuperContainerComplete[int]([3, 4, 5])\nreveal_type(sc)\n\nsc2 = sc.foo()\nreveal_type(sc2)\n\nprint(sc2.time_travel())\n
Run Code Online (Sandbox Code Playgroud)\n

将所有内容放在一起,我们得到:

\n
from typing import TypeVar, Generic, Any, TypeAlias, TYPE_CHECKING\n\nif not TYPE_CHECKING:\n    reveal_type = print\n\nT = TypeVar(\'T\')\nSelfStr = TypeVar("SelfStr", bound="Container[str, Any]", covariant=True)\n\nclass Container(Generic[T, SelfStr]):\n    def __init__(self, contents: list[T]):\n        self._contents = contents\n\n    def __iter__(self):\n        return iter(self._contents)\n\n    def foo(self) -> SelfStr:\n        reveal_type(type(self))\n        # Mypy is wrong here: it thinks that type(self) is already annotated, but in fact the type parameters are erased.\n        return type(self)([str(x) for x in self])  # type: ignore\n\n    def __repr__(self):\n        return type(self).__name__ + "(" + repr(self._contents) + ")"\n_ContainerStr: TypeAlias = Container[str, "_ContainerStr"]\nContainerComplete: TypeAlias = Container[T, _ContainerStr]\n\nclass SuperContainer(Container[T, SelfStr]):\n    def time_travel(self):\n        return "magic"\n_SuperContainerStr: TypeAlias = SuperContainer[str, "_SuperContainerStr"]\nSuperContainerComplete: TypeAlias = SuperContainer[T, _SuperContainerStr]\n\nsc = SuperContainerComplete[int]([3, 4, 5])\nreveal_type(sc)\n\nsc2 = sc.foo()\nreveal_type(sc2)\n\nprint(sc2.time_travel())\n
Run Code Online (Sandbox Code Playgroud)\n

输出如下所示(您需要最新版本的 mypy):

\n
$ mypy test.py\ntest.py:17: note: Revealed type is "Type[test.Container[T`1, SelfStr`2]]"\ntest.py:33: note: Revealed type is "test.SuperContainer[builtins.int, test.SuperContainer[builtins.str, ...]]"\ntest.py:36: note: Revealed type is "test.SuperContainer[builtins.str, test.SuperContainer[builtins.str, ...]]"\nSuccess: no issues found in 1 source file\n$ python test.py\n<__main__.SuperContainer object at 0x7f30165582d0>\n<class \'__main__.SuperContainer\'>\n<__main__.SuperContainer object at 0x7f3016558390>\nmagic\n$\n
Run Code Online (Sandbox Code Playgroud)\n

您可以使用元类删除大量样板文件。这还有一个额外的优点,那就是它是继承的。如果您覆盖__call__,您甚至可以isinstance正常工作(它不适用于泛型类型别名*Complete,它仍然适用于类本身)。

\n

请注意,这仅在 PyCharm 中部分有效:\n它不会报告 SuperContainer.foo().foo().time_travel() 上的警告

\n