【问题标题】:Changing type argument of Python with subclassing通过子类化改变 Python 的类型参数
【发布时间】:2022-12-17 14:09:54
【问题描述】:

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

class A(Generic[T]):
    def get_next(self) -> T

这非常方便。但是,即使在 3.11 中使用 Self 类型,我也无法在不指定类名的情况下找到更改类型参数(T)的方法。这是 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])

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

class SuperContainer(Container[T]):
    def time_travel(self): ...

然后,如果我有一个 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

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

【问题讨论】:

  • 我猜我会说不。 python 不支持依赖于其他泛型的泛型。这很可能是this postthis mypy issue中提出的限制的一个例子

标签: python generics subclass mypy typing


【解决方案1】:

改变的一种方法超类中的类型参数在保留子类类型的同时,使用类型模块中的 TypeVar 类。此类允许您定义一个类型变量,该变量可用于类层次结构中以指定 generic 类的类型。

您可以通过在 ContainerSuperContainer 类中使用 TypeVar 来执行此操作:

from typing import TypeVar, Generic
T = TypeVar('T')
class Container(Generic[T]):
    def foo(self: Container[T]) -> Container[str]:
        return self.__class__([str(x) for x in self])

class SuperContainer(Container[T]):
    def time_travel(self):
        pass

sc = SuperContainer([3, 4, 5])
sc2 = sc.foo()

reveal_type(sc2)
sc2.time_travel()
isinstance(sc2, SuperContainer)

【讨论】:

  • 我认为您需要先通过 mypy 运行您的代码,然后再将其作为错误发布。当你运行sc2 = sc.foo()时,mypy 认为 sc2 是一个容器,因此不能调用 time_travel。这是问题中描述的确切问题。
【解决方案2】:

要解决这个问题,您需要第二个泛型类型参数,以表示foo 的返回类型。

SelfStr = TypeVar("SelfStr", bound="Container[str, Any]", covariant=True)

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

class Container(Generic[T, SelfStr]):
    def __init__(self, contents: list[T]):
        self._contents = contents

    def __iter__(self):
        return iter(self._contents)

    def foo(self) -> SelfStr:
        reveal_type(type(self))
        # Mypy is wrong here: it thinks that type(self) is already annotated, but in fact the type parameters are erased.
        return type(self)([str(x) for x in self])  # type: ignore

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

接下来,我们需要一些创建容器的方法。我们希望类型看起来像Container[T, Container[str, Container[str, ...]]]。一些✨ magic ✨的时间。

_ContainerStr: TypeAlias = Container[str, "_ContainerStr"]
ContainerComplete: TypeAlias = Container[T, _ContainerStr]

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

ContainerComplete[int]([1,2,3])

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

class SuperContainer(Container[T, SelfStr]):
    def time_travel(self):
        return "magic"
_SuperContainerStr: TypeAlias = SuperContainer[str, "_SuperContainerStr"]
SuperContainerComplete: TypeAlias = SuperContainer[T, _SuperContainerStr]

全部完成!现在让我们演示一下:

sc = SuperContainerComplete[int]([3, 4, 5])
reveal_type(sc)

sc2 = sc.foo()
reveal_type(sc2)

print(sc2.time_travel())

把所有东西放在一起,我们得到:

from typing import TypeVar, Generic, Any, TypeAlias, TYPE_CHECKING

if not TYPE_CHECKING:
    reveal_type = print

T = TypeVar('T')
SelfStr = TypeVar("SelfStr", bound="Container[str, Any]", covariant=True)

class Container(Generic[T, SelfStr]):
    def __init__(self, contents: list[T]):
        self._contents = contents

    def __iter__(self):
        return iter(self._contents)

    def foo(self) -> SelfStr:
        reveal_type(type(self))
        # Mypy is wrong here: it thinks that type(self) is already annotated, but in fact the type parameters are erased.
        return type(self)([str(x) for x in self])  # type: ignore

    def __repr__(self):
        return type(self).__name__ + "(" + repr(self._contents) + ")"
_ContainerStr: TypeAlias = Container[str, "_ContainerStr"]
ContainerComplete: TypeAlias = Container[T, _ContainerStr]

class SuperContainer(Container[T, SelfStr]):
    def time_travel(self):
        return "magic"
_SuperContainerStr: TypeAlias = SuperContainer[str, "_SuperContainerStr"]
SuperContainerComplete: TypeAlias = SuperContainer[T, _SuperContainerStr]

sc = SuperContainerComplete[int]([3, 4, 5])
reveal_type(sc)

sc2 = sc.foo()
reveal_type(sc2)

print(sc2.time_travel())

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

$ mypy test.py
test.py:17: note: Revealed type is "Type[test.Container[T`1, SelfStr`2]]"
test.py:33: note: Revealed type is "test.SuperContainer[builtins.int, test.SuperContainer[builtins.str, ...]]"
test.py:36: note: Revealed type is "test.SuperContainer[builtins.str, test.SuperContainer[builtins.str, ...]]"
Success: no issues found in 1 source file
$ python test.py
<__main__.SuperContainer object at 0x7f30165582d0>
<class '__main__.SuperContainer'>
<__main__.SuperContainer object at 0x7f3016558390>
magic
$

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

【讨论】:

    猜你喜欢
    • 2015-07-12
    • 1970-01-01
    • 2015-04-26
    • 2011-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多