mypy 是不是有子类可接受的返回类型?
Posted
技术标签:
【中文标题】mypy 是不是有子类可接受的返回类型?【英文标题】:Does mypy have a Subclass-Acceptable Return Type?mypy 是否有子类可接受的返回类型? 【发布时间】:2019-08-21 19:43:49 【问题描述】:我想知道如何(或目前是否可能)表示函数将返回 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
,或类似的东西。
【问题讨论】:
不,我不想做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 是不是有子类可接受的返回类型?的主要内容,如果未能解决你的问题,请参考以下文章