我想知道如何(或者当前是否可能)表示函数将返回mypy可接受的特定类的子类?
这是一个简单的例子,其中基类Foo
由Bar
和Baz
继承,并且有一个便利函数create()
将返回Foo
的子类(Bar
或Baz
),具体取决于指定的参数:
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
,或类似的东西。
Mypy并没有抱怨你定义你的功能的方式:那个部分实际上是完全没有错误的。
相反,它抱怨你在最后一行的变量赋值中调用函数的方式:
bar: Bar = create('bar')
由于create(...)
注释为返回Foo
或foo的任何子类,因此将其分配给Bar
类型的变量并不保证是安全的。你在这里的选择是删除注释(并接受bar
将是Foo
类型),直接将函数的输出转换为Bar
,或者完全重新设计代码以避免这个问题。
如果你想让mypy理解当你传入字符串create
时,Bar
将特别返回"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]()
但就个人而言,我会对过度使用这种模式持谨慎态度 - 老实说,我认为频繁使用这些类型的恶作剧是一种代码味道。此解决方案也不支持特殊套管任意数量的子类型:您必须为每个子类型创建一个重载变量,这可能会变得非常笨重和冗长。
因此,经过进一步研究(以及此处的其他回复),对我的问题的简短回答似乎是没有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接受了赋值而没有错误。它仍然不是一个理想的解决方法,但它提供的所有解决方案最接近我所期望的用途。