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

qojgxg4l  于 2023-05-27  发布在  Python
关注(0)|答案(3)|浏览(97)

我想知道如何(或者目前是否可能)表达一个函数将返回一个特定类的子类,这是我可以接受的?
下面是一个简单的例子,其中基类FooBarBaz继承,并且有一个方便的函数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')

使用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[...]类型大的数字。
有人知道如何以非复杂的方式做到这一点吗?
在可能的情况下,没有非复杂的方法来做到这一点,我知道一些不太理想的方法来规避这个问题:
1.一般化返回类型:

def create(kind: str) -> typing.Any:
    ...

这解决了赋值时的类型问题,但这是一个麻烦,因为它减少了函数签名返回的类型信息。
1.忽略错误:

bar: Bar = create('bar')  # type: ignore

这抑制了mypy错误,但这也不是理想的。我确实喜欢它,它使它更明确地表明,bar: Bar = ...是故意的,而不仅仅是一个编码错误,但抑制错误仍然不太理想。
1.强制转换类型:

bar: Bar = typing.cast(Bar, create('bar'))

与前一个例子一样,这个例子的积极方面是它使Foo返回到Bar赋值更加明确。这可能是最好的选择,如果没有办法做到我上面的要求。我认为我讨厌使用它的部分原因是作为一个 Package 函数的笨拙(在使用和可读性方面)。可能只是现实,因为类型转换不是语言的一部分-例如create('bar') as Bar,或create('bar') astype Bar,或沿着的东西。

hc8w905p

hc8w905p1#

我并不是在抱怨你定义函数的方式:那部分实际上完全没问题,没有错误。
相反,它在抱怨你在最后一行的变量赋值中 * 调用 * 函数的方式:

bar: Bar = create('bar')

由于create(...)被注解为返回Foo或foo的任何子类,因此将其分配给Bar类型的变量并不保证安全。您的选择是删除注解(并接受bar将是Foo类型),直接将函数的输出转换为Bar,或者重新设计代码以避免此问题。
如果你想让我明白,当你传入字符串"bar"时,create将具体返回一个Bar,你可以通过组合重载和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]()

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

eyh26e7m

eyh26e7m2#

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)

有必要传入你想要示例化的类类型,这是有意义的,因为mypy是一个静态类型检查器,不做运行时类型检查。
也就是说,当输入是动态的时,将变量bar指定为Bar类类型没有帮助,可能会改为'baz'。
另一方面,动态地选择要示例化的类类型是很有用的。因此,不要让它“看起来”是动态的,但仍然将变量类型硬编码为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

原始答案(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')
o4hqfura

o4hqfura3#

我用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是“协变的”,尽管我不是Maven,但上面的链接指向PEP484以获得更多细节

相关问题