通过子类别更改Python的类型参数

发布于 2025-01-24 06:29:17 字数 1148 浏览 0 评论 0原文

Python的打字系统允许在类中进行通用:

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

这非常方便。但是,即使在3.11中使用自我类型,我也找不到一种方法来更改类型参数(t),而无需指定类名。这是PEP 673的推荐用法: https://peps.pys.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): ...

然后,如果我有一个超本机的实例并在上面调用foo,那么打字将是错误的,并且认为它是一个不是超级城市的容器。

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's typing system allows for generics in classes:

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

which is very handy. However, even in 3.11 with the Self type, I cannot find a way to change the type argument (the T) without specifying the class name. Here's the recommended usage from PEP 673: Self Type: 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])

The problem is if I want to subclass container:

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

And then if I have an instance of SuperContainer and call foo on it, the typing will be wrong, and think that it's a Container not 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

Is there an accepted way to allow a program to change the type argument in the superclass that preserves the typing of the subclass?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

说不完的你爱 2025-01-31 06:29:17

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

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) + ")"

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

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

在类声明的第一行中,我们使该类的第二个类型参数是selfstr,它本身就是容器[str,any any]。这意味着selfstr的定义应与container [str,selfstr]有限,因此我们应该获得container的上限containe [str,container [str [str,str,str, ...]]我们想要。这起作用:它将仅允许我们的递归类型(或子类)任何 。不幸的是,Mypy不会推断递归通用类型,报告test.container [hindins.int,< nothing>],因此我们必须进行繁重的举重。时间✨魔术✨

_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
$

您可以使用Metaclasses删除很多样板。这具有继承的额外优势。如果您覆盖__调用__,您甚至可以获取isInstance正常工作(它不适用于通用类型别名 *完整,它仍然有效可以自己上课)。

请注意,这仅在Pycharm中部分起作用:

To solve this, you need a second generic type argument, to represent the return type of foo.

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

The Any is okay. We'll see that later.

So far so good. Let's define the 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

    def __repr__(self):
        return type(self).__name__ + "(" + repr(self._contents) + ")"

Note that we had to ignore the types in foo. This is because mypy has inferred the type of type(self) incorrectly. It thinks that type(self) returns Container[...] (or a subclass), but in fact it returns Container (or a subclass). You'll see that when we get to running this code.

Next, we need some way of creating a container. We want the type to look like Container[T, Container[str, Container[str, ...]]].

In the first line of the class declaration, we made the second type-parameter of the class be a SelfStr, which is itself Container[str, Any]. This means that definition of SelfStr should become bounded to Container[str, SelfStr], so we should get an upper bound of Container[str, Container[str, ...]] as we wanted. This works: it will only allow our recursive type (or subclasses) or Any. Unfortunately, mypy won't do inference on recursive generic types, reporting test.Container[builtins.int, <nothing>], so we have to do the heavy lifting. Time for some ✨ magic ✨.

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

The _ContainerStr alias will give us the recursive part of the signature. We then expose ContainerComplete, which we can use as a constructor, for example:

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

Awesome! But what about subclasses? We just have to do the same thing again, for our subclass:

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

All done! Now let's demonstrate:

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

sc2 = sc.foo()
reveal_type(sc2)

print(sc2.time_travel())

Putting everything together, we get:

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())

And the output looks like this (you need a recent version of 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
$

You can remove a lot of the boilerplate using metaclasses. This has the added advantage that it's inherited. If you override __call__, you can even get isinstance working properly (it doesn't work with the generic type aliases *Complete, it still works fine for the classes themselves).

Note that this only partially works in PyCharm:
it does not report warnings on SuperContainer.foo().foo().time_travel()

栖迟 2025-01-31 06:29:17

我不知道它是如何工作的,但是我也可以使用type [t]来使用。我真的无法解释此代码,所以我只是要复制和粘贴,比我能告诉你的更好的人为什么

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

if not TYPE_CHECKING:
    reveal_type = print

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

class Container(Generic[T, SelfStr, SelfTypeT]):
    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 get_types(self) -> SelfTypeT:
        return type(self)([type(x) for x in self])  # type: ignore

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

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

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

sc2 = sc.foo()
reveal_type(sc2)

sc3 = sc.get_types()
reveal_type(sc3)

class Base:
    pass

class Impl1(Base):
    pass

class Impl2(Base):
    pass

sc4 = SuperContainerComplete[Base]([Impl1(), Impl2()])

sc5 = sc4.foo()
reveal_type(sc5)

sc6 = sc4.get_types()
reveal_type(sc6)

print(sc2.time_travel())

Mypy和Python都对此代码感到满意,因此我猜它有效(TM)。

I have no clue how this works, but I made it work with Type[T] too. I genuinely cannot explain this code so I'm just gonna copy and paste, better people than me can tell you why.

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

if not TYPE_CHECKING:
    reveal_type = print

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

class Container(Generic[T, SelfStr, SelfTypeT]):
    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 get_types(self) -> SelfTypeT:
        return type(self)([type(x) for x in self])  # type: ignore

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

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

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

sc2 = sc.foo()
reveal_type(sc2)

sc3 = sc.get_types()
reveal_type(sc3)

class Base:
    pass

class Impl1(Base):
    pass

class Impl2(Base):
    pass

sc4 = SuperContainerComplete[Base]([Impl1(), Impl2()])

sc5 = sc4.foo()
reveal_type(sc5)

sc6 = sc4.get_types()
reveal_type(sc6)

print(sc2.time_travel())

Mypy and Python are both happy with this code so I guess It Works (TM).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文