Python 覆盖子类中方法返回的类型提示,无需重新定义方法签名

Posted

技术标签:

【中文标题】Python 覆盖子类中方法返回的类型提示,无需重新定义方法签名【英文标题】:Python overriding type hint on a method's return in child class, without redefining method signature 【发布时间】:2020-07-23 13:31:18 【问题描述】:

我有一个基类,其方法返回时的类型提示为 float

在子类中,如果不重新定义签名,我可以以某种方式将方法返回的类型提示更新为int吗?


示例代码

#!/usr/bin/env python3.6


class SomeClass:
    """This class's some_method will return float."""

    RET_TYPE = float

    def some_method(self, some_input: str) -> float:
        return self.RET_TYPE(some_input)


class SomeChildClass(SomeClass):
    """This class's some_method will return int."""

    RET_TYPE = int


if __name__ == "__main__":
    ret: int = SomeChildClass().some_method("42"). # 
    ret2: float = SomeChildClass().some_method("42")

我的 IDE 抱怨类型不匹配:

发生这种情况是因为我的 IDE 仍在使用来自 SomeClass.some_method 的类型提示。


研究

我认为解决方案可能是使用泛型,但我不确定是否有更简单的方法。

Python: how to override type hint on an instance attribute in a subclass?

建议可能使用instance variable annotations,但我不确定如何为返回类型执行此操作。

【问题讨论】:

我认为这不可能,因为在您的情况下,只有一个some_methodSomeClass 上定义的那个)。 SomeChildClass 的实例会将方法名称解析为在 SomeClass 上定义的同一个函数对象,它们实际上没有自己的单独实现,您可以单独注释。注解只是函数对象上的一个属性,它不能真正有不同的注解,具体取决于它是如何通过 MRO 解决的。 啊,这很有道理@wim,谢谢你的回复。您对更新子类(SomeChildClass)中的注释的最简单方法有什么想法吗? 嗯,最简单的方法:与其做RET_TYPE = float类属性的事情,不如在子类中重新定义方法? 添加到对话中:如果您将行更改为 def some_method(self, some_input: str) -> RET_TYPE: (因此将 'float' 类型提示替换为 'RET_TYPE' 提示,然后视图中的错误指示消失了(不幸的是,不在 main 中),至少在我的 PyCharm 中。显然 PyCharm 足够聪明,可以发现每次调用的 RET_TYPE 都不同。 【参考方案1】:

以下代码在 PyCharm 上运行良好。我添加了complex 案例以使其更清晰。

我基本上将方法提取到泛型类中,然后将其用作每个子类的混合。请小心使用,因为它似乎很不标准。

from typing import ClassVar, Generic, TypeVar, Callable


S = TypeVar('S', bound=complex)


class SomeMethodImplementor(Generic[S]):
    RET_TYPE: ClassVar[Callable]

    def some_method(self, some_input: str) -> S:
        return self.__class__.RET_TYPE(some_input)


class SomeClass(SomeMethodImplementor[complex]):
    RET_TYPE = complex


class SomeChildClass(SomeClass, SomeMethodImplementor[float]):
    RET_TYPE = float


class OtherChildClass(SomeChildClass, SomeMethodImplementor[int]):
    RET_TYPE = int


if __name__ == "__main__":
    ret: complex = SomeClass().some_method("42")
    ret2: float = SomeChildClass().some_method("42")
    ret3: int = OtherChildClass().some_method("42")
    print(ret, type(ret), ret2, type(ret2), ret3, type(ret3))

例如,如果您将ret2: float 更改为ret2: int,它将正确显示类型错误。

遗憾的是,mypy 确实在这种情况下显示错误(版本 0.770),

otherhint.py:20: error: Incompatible types in assignment (expression has type "Type[float]", base class "SomeClass" defined the type as "Type[complex]")
otherhint.py:24: error: Incompatible types in assignment (expression has type "Type[int]", base class "SomeClass" defined the type as "Type[complex]")
otherhint.py:29: error: Incompatible types in assignment (expression has type "complex", variable has type "float")
otherhint.py:30: error: Incompatible types in assignment (expression has type "complex", variable has type "int")

第一个错误可以通过写作“修复”

    RET_TYPE: ClassVar[Callable] = int

对于每个子类。现在,错误减少到

otherhint.py:29: error: Incompatible types in assignment (expression has type "complex", variable has type "float")
otherhint.py:30: error: Incompatible types in assignment (expression has type "complex", variable has type "int")

这与我们想要的正好相反,但如果你只关心 PyCharm,那也没关系。

【讨论】:

感谢@KevinLanguasco 的回答!我不是最好的程序员,你能解释一下为什么你需要使用bound=complexS = TypeVar('S', bound=complex)吗? 哦,实际上不需要!只是说替换 S 的每个具体类型都必须是复数的子类型。所以如果你写了,比如说,SomeMethodImplementor[str],它会发送一个警告,因为str不是complex的子类型。只是额外的类型安全:)python.org/dev/peps/pep-0484/…(我使用intfloat的子类型,这是complex的子类型) 好的@KevinLanguasco 我奖励你是因为你的回答填补了我错过的很多漏洞。如果您好奇/有任何想法,我能够找到一个混合解决方案(我已发布),它也不会在 mypy 中产生错误【参考方案2】:

你可以使用类似的东西:

from typing import TypeVar, Generic


T = TypeVar('T', float, int) # types you support


class SomeClass(Generic[T]):
    """This class's some_method will return float."""

    RET_TYPE = float

    def some_method(self, some_input: str) -> T:
        return self.RET_TYPE(some_input)


class SomeChildClass(SomeClass[int]):
    """This class's some_method will return int."""

    RET_TYPE = int


if __name__ == "__main__":
    ret: int = SomeChildClass().some_method("42")
    ret2: float = SomeChildClass().some_method("42")

但是有一个问题。那我不知道怎么解决。对于 SomeChildClass 方法 some_method IDE 将显示通用提示。至少 pycharm(我想你是这个)不会将其显示为错误。

【讨论】:

感谢@AntonPomieshchenko 的回答!【参考方案3】:

好的,所以我可以尝试并结合@AntonPomieshcheko 和@KevinLanguasco 的答案来提出解决方案:

我的 IDE (PyCharm) 可以正确推断返回类型 mypy 报告类型是否不匹配 在运行时不会出错,即使类型提示指示不匹配也是如此

这正是我想要的行为。非常感谢大家:)

#!/usr/bin/env python3

from typing import TypeVar, Generic, ClassVar, Callable


T = TypeVar("T", float, int)  # types supported


class SomeBaseClass(Generic[T]):
    """This base class's some_method will return a supported type."""

    RET_TYPE: ClassVar[Callable]

    def some_method(self, some_input: str) -> T:
        return self.RET_TYPE(some_input)


class SomeChildClass1(SomeBaseClass[float]):
    """This child class's some_method will return a float."""

    RET_TYPE = float


class SomeChildClass2(SomeBaseClass[int]):
    """This child class's some_method will return an int."""

    RET_TYPE = int


class SomeChildClass3(SomeBaseClass[complex]):
    """This child class's some_method will return a complex."""

    RET_TYPE = complex


if __name__ == "__main__":
    some_class_1_ret: float = SomeChildClass1().some_method("42")
    some_class_2_ret: int = SomeChildClass2().some_method("42")

    # PyCharm can infer this return is a complex.  However, running mypy on
    # this will report (this is desirable to me):
    # error: Value of type variable "T" of "SomeBaseClass" cannot be "complex"
    some_class_3_ret = SomeChildClass3().some_method("42")

    print(
        f"some_class_1_ret = some_class_1_ret of type type(some_class_1_ret)\n"
        f"some_class_2_ret = some_class_2_ret of type type(some_class_2_ret)\n"
        f"some_class_3_ret = some_class_3_ret of type type(some_class_3_ret)\n"
    )

【讨论】:

如果它适合您的用例,则可以使用,但请注意,如果您想拥有像class SomeChildClass2(SomeChildClass1): 这样的子类层次结构(如问题中所建议的那样),那么您基本上会从您开始的地方结束。跨度>

以上是关于Python 覆盖子类中方法返回的类型提示,无需重新定义方法签名的主要内容,如果未能解决你的问题,请参考以下文章

Java继承方法隐藏(覆盖)

重写(覆盖,Override)和重载(Overload)的区别

java-协变返回类型

java-协变返回类型

Python 类型提示 - 为 dict 子类指定键、值类型

Swift - 在超类的覆盖函数中返回子类类型