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

时间:2019-03-31 13:52:41

标签: python typing mypy

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

这是一个简单的示例,其中FooBar继承了基类Baz,并且有一个便利函数create()会返回{{1}的子类}(FooBar),取决于指定的参数:

Baz

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

错误:分配中的类型不兼容(表达式的类型为“ Foo”,变量的类型为“ Bar”)

有没有办法表明这应该是可接受/允许的。 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') 函数的预期返回不是(或可能不是)create()的实例,而是它的子类?

我正在寻找类似的东西:

Foo

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

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

但是我正在寻找一种可以归纳为N个可能的子类的东西,其中N是一个比我想定义为def create(kind: str) -> typing.Union[Bar, Baz]: choices = {'bar': Bar, 'baz': Baz} return choices[kind]() 类型的数字大的数字。

有人对如何以一种不费吹灰之力的方式有想法吗?


在可能的情况下,没有没有复杂的方法可以做到这一点,我知道有很多不理想的方法可以解决此问题:

  1. 概括返回类型:
typing.Union[...]

这解决了赋值的键入问题,但令人讨厌,因为它减少了函数签名返回的类型信息。

  1. 忽略错误:
def create(kind: str) -> typing.Any:
    ...

这可以抑制mypy错误,但这也不理想。我确实喜欢这样做,这使得bar: Bar = create('bar') # type: ignore 是有意的,而不仅仅是编码错误,但抑制该错误仍然不是理想的情况。

  1. 投射类型:
bar: Bar = ...

与前面的情况一样,该情况的积极方面是,它使bar: Bar = typing.cast(Bar, create('bar')) 返回到Foo的分配更加有意地明确了。如果没有办法完成我上面要问的问题,这可能是最好的选择。我认为我不喜欢使用它的一部分是笨拙(在用法和可读性上)作为包装函数。由于类型转换不是语言的一部分,例如Barcreate('bar') as Bar或类似的语言,所以这可能只是现实。

4 个答案:

答案 0 :(得分:1)

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

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

bar: Bar = create('bar')

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


如果您希望mypy理解当您传递字符串createBar将特别返回"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]()

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

答案 1 :(得分:1)

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

答案 2 :(得分:0)

我使用 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 以获取更多详细信息

答案 3 :(得分:-1)

因此,根据进一步的研究(以及此处的其他回答),对我的问题的简短回答似乎是,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接受分配而没有错误。它仍然不是理想的解决方法,但是在所有解决方案中,它提供的功能与我一直在寻找的最接近。