mypy 是否有 Subclass-Acceptable Return 类型?

Does mypy have a Subclass-Acceptable Return Type?

我想知道如何(或者目前是否可能)表达一个函数将 return 是 mypy 可接受的特定 class 的子class?

这是一个简单的例子,其中基础 class FooBarBaz 继承,并且有一个方便的函数 create() 将 return Foo 的子 class(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 检查此代码时,出现以下错误 returned:

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

有没有办法表明这应该是 acceptable/allowable。 create() 函数的预期 return 不是(或可能不是)Foo 的实例,而是它的子 class?

我在寻找类似的东西:

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 个可能的子 class 的东西,其中 N 是一个比我想定义为 typing.Union[...] 类型的数字更大的数字。

有人知道如何以简单的方式做到这一点吗?


在可能没有简单方法的情况下,我知道有许多不太理想的方法可以规避这个问题:

  1. 泛化 return 类型:
def create(kind: str) -> typing.Any:
    ...

这解决了赋值的类型问题,但令人失望,因为它减少了函数签名的类型信息 return。

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

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

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

与前一个案例一样,这个案例的积极方面是它使 Foo return 到 Bar 的赋值更加明确。如果无法执行我上面的要求,这可能是最好的选择。我认为我厌恶使用它的部分原因是作为包装函数的笨拙(在使用和可读性方面)。可能只是现实,因为类型转换不是语言的一部分 - 例如 create('bar') as Bar,或 create('bar') astype Bar,或类似的东西。

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

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

bar: Bar = create('bar')

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


如果你想让 mypy 理解当你传入字符串 "bar"create 将 return 特别是 Bar,你可以通过合并 overloads and 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]()

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

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

但是,我确实发现第四个 option/workaround 比我在问题中包含的前三个解决方法更令人满意。那就是将基数 class 与 typing.Any 合并。这允许更灵活地指定赋值中的类型定义,同时在不被赋值覆盖的情况下保留基 class 的类型提示。解决方法如下所示:

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


bar: Bar = create('bar')

结果是mypy毫无错误地接受了赋值。它仍然不是一个理想的解决方法,但在所有解决方案中,它提供的最接近我正在寻找的所需用法。

我使用 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 是“协变的”,虽然我不是专家,但上面的 link 指向 PEP484 以获取更多详细信息

你可以找到答案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')