mypy 是不是有子类可接受的返回类型?

Posted

技术标签:

【中文标题】mypy 是不是有子类可接受的返回类型?【英文标题】:Does mypy have a Subclass-Acceptable Return Type?mypy 是否有子类可接受的返回类型? 【发布时间】:2019-08-21 19:43:49 【问题描述】:

我想知道如何(或目前是否可能)表示函数将返回 mypy 可接受的特定类的子类?

这是一个简单的示例,其中基类FooBarBaz 继承,并且有一个便利函数create() 将返回Foo 的子类(BarBaz ) 取决于指定的参数:

class Foo:
    pass


class Bar(Foo):
    pass


class Baz(Foo):
    pass


def create(kind: str) -> Foo:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()


bar: Bar = create('bar')

使用 mypy 检查此代码时,返回以下错误:

错误:赋值类型不兼容(表达式类型为“Foo”,变量类型为“Bar”)

有没有办法表明这应该是可接受/允许的。 create() 函数的预期返回不是(或可能不是)Foo 的实例,而是它的子类?

我正在寻找类似的东西:

def create(kind: str) -> typing.Subclass[Foo]:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()

但这并不存在。显然,在这种简单的情况下,我可以这样做:

def create(kind: str) -> typing.Union[Bar, Baz]:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()

但我正在寻找可以推广到 N 个可能子类的东西,其中 N 是一个大于我想要定义为 typing.Union[...] 类型的数字。

有人对如何以不复杂的方式做到这一点有任何想法吗?


如果没有不复杂的方法来做到这一点,我知道有一些不太理想的方法来规避这个问题:

    概括返回类型:
def create(kind: str) -> typing.Any:
    ...

这解决了赋值的类型问题,但很糟糕,因为它减少了函数签名返回的类型信息。

    忽略错误:
bar: Bar = create('bar')  # type: ignore

这会抑制 mypy 错误,但这也不理想。我确实喜欢它更明确地表明bar: Bar = ... 是故意的,而不仅仅是编码错误,但抑制错误仍然不太理想。

    投射类型:
bar: Bar = typing.cast(Bar, create('bar'))

与前一种情况一样,这种情况的积极方面是它使Foo 返回到Bar 分配更加有意明确。如果无法按照我上面的要求进行操作,这可能是最好的选择。我认为我不喜欢使用它的部分原因是作为包装函数的笨拙(在使用和可读性方面)。可能只是现实,因为类型转换不是语言的一部分 - 例如create('bar') as Bar,或create('bar') astype Bar,或类似的东西。

【问题讨论】:

不,我不想做foo: Foo = create('bar'),因为在任何实际场景中(不是我上面创建的过于简化的场景)我想利用Bar子类中提供的功能存在于父类Foo 【参考方案1】:

Mypy 并没有抱怨您定义函数的方式:该部分实际上完全没有错误。

相反,它抱怨您在最后一行的变量赋值中调用函数的方式:

bar: Bar = create('bar')

由于create(...) 被注释为返回Foo 或foo 的任何子类,因此不能保证将其分配给Bar 类型的变量是安全的。您可以在此处选择删除注释(并接受 bar 的类型为 Foo),直接将函数的输出转换为 Bar,或者完全重新设计代码以避免此问题。


如果您希望 mypy 在您传入字符串 "bar" 时了解 create 将专门返回 Bar,您可以通过组合 overloads 和 Literal types 将其组合在一起。例如。你可以这样做:

from typing import overload
from typing_extensions import Literal   # You need to pip-install this package

class Foo: pass
class Bar(Foo): pass
class Baz(Foo): pass

@overload
def create(kind: Literal["bar"]) -> Bar: ...
@overload
def create(kind: Literal["baz"]) -> Baz: ...
def create(kind: str) -> Foo:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()

但就个人而言,我对过度使用这种模式持谨慎态度——老实说,我认为频繁使用这些类型的恶作剧是一种代码味道。此解决方案也不支持特殊大小写任意数量的子类型:您必须为每个子类型创建一个重载变体,这会变得非常庞大和冗长。

【讨论】:

Foo 的类型提示并不表示Foo 或其子类,您需要使用typing.TypeVar 处理这种类型的事情【参考方案2】:

您可以找到答案here。本质上,您需要这样做:

class Foo:
    pass


class Bar(Foo):
    pass


class Baz(Foo):
    pass

from typing import TypeVar
U = TypeVar('U', bound=Foo)

def create(kind: str) -> U:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()


bar: Bar = create('bar')

【讨论】:

【参考方案3】:

我使用typing.Type 解决了类似的问题。对于您的情况,我会这样使用它:

class Foo:
    pass

class Bar(Foo):
    pass

class Baz(Foo):
    pass

def create(kind: str) -> typing.Type[Foo]:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()

这似乎可行,因为Type 是“协变的”,虽然我不是专家,但上述链接指向PEP484 了解更多详情

【讨论】:

【参考方案4】:

因此,经过进一步研究(以及此处的其他回复),我的问题的简短回答似乎是否定的,mypy/typing 中没有直接支持此案例的功能。

但是,我确实找到了第四个选项/解决方法,我发现它比我在问题中包含的前三个解决方法更令人满意。也就是将基类与typing.Any 联合起来。这允许更灵活地指定分配中的类型定义,同时保留基类的类型提示(如果没有被分配覆盖)。解决方法如下所示:

def create(kind: str) -> typing.Union[Foo, typing.Any]:
    choices = 'bar': Bar, 'baz': Baz
    return choices[kind]()


bar: Bar = create('bar')

结果是 mypy 没有错误地接受分配。它仍然不是一个理想的解决方法,但它提供的所有解决方案最接近我正在寻找的所需用途。

【讨论】:

这实际上可以在没有联合的情况下工作:def create(kind: str) -> typing.Any. 可能你想要的可以用一个协议类来完成,但它不是一回事。见:mypy.readthedocs.io/en/stable/protocols.html

以上是关于mypy 是不是有子类可接受的返回类型?的主要内容,如果未能解决你的问题,请参考以下文章

Mypy 子类中更具体的参数

一种将 NamedTuple 子类化以进行类型检查的方法

java里,为啥子类不可以有 和父类 同名不同返回类型 的方法?

8.2java 方法重写和属性重写

使用“类”数据类型时,如何指定类型以便只接受特定类的子类?

java-协变返回类型