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

Ser*_*nst 10 python type-hinting mypy

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

这是一个简单的示例,其中基类FooBarand继承,Baz并且有一个方便的函数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')
Run Code Online (Sandbox Code Playgroud)

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

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

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

我正在寻找类似的东西:

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

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

def create(kind: str) -> typing.Union[Bar, Baz]:
    choices = {'bar': Bar, 'baz': Baz}
    return choices[kind]()
Run Code Online (Sandbox Code Playgroud)

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

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


在可能没有非复杂的方法来做到这一点的情况下,我知道一些不太理想的方法来规避这个问题:

  1. 概括返回类型:
def create(kind: str) -> typing.Any:
    ...
Run Code Online (Sandbox Code Playgroud)

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

  1. 忽略错误:
bar: Bar = create('bar')  # type: ignore
Run Code Online (Sandbox Code Playgroud)

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

  1. 投射类型:
bar: Bar = typing.cast(Bar, create('bar'))
Run Code Online (Sandbox Code Playgroud)

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

Mic*_*x2a 10

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

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

bar: Bar = create('bar')
Run Code Online (Sandbox Code Playgroud)

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


如果您希望 mypy 理解在您传入字符串时create将专门返回 a ,您可以通过组合重载Literal 类型来将其组合在一起。例如,您可以执行以下操作:Bar"bar"

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]()
Run Code Online (Sandbox Code Playgroud)

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


Lim*_*iat 9

2023 年更新:mypy 1.3.0

根据 PEP484 你需要做这样的事情:

class Foo:
    pass

class Bar(Foo):
    pass

class Baz(Foo):
    pass

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

def create(kind: type[U]) -> U:
    return kind()

bar: Bar = create(Bar)
Run Code Online (Sandbox Code Playgroud)

有必要传入要实例化的类类型,这是有道理的,因为 mypy 是静态类型检查器并且不执行运行时类型检查。

Bar即,当输入是动态的并且可能是“baz”时,将变量 bar 指定为类类型没有帮助。

另一方面,动态选择要实例化的类类型很有用。因此,不要使其“看起来”动态,但仍将变量类型硬编码为Bar,而是将变量类型指定为基类。

然后通过在运行时检查类类型来执行业务逻辑所需的任何操作:

class Foo:
    pass

class Bar(Foo):
    pass

class Baz(Foo):
    pass

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

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

bar: Foo = create('bar')
print(bar.__class__.__name__)  # Bar
Run Code Online (Sandbox Code Playgroud)

原始答案(2021 年 3 月)

您可以在这里找到答案。本质上,您需要执行以下操作:

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')
Run Code Online (Sandbox Code Playgroud)