如何将 return 类型注释为 class 实例或其(唯一)子 class 实例?

How to annotate a return type as either a class instance or its (unique) subclass instance?

我正在编写一个 python 库,通过导入和(可选)子 classing 它提供的一些 'helper classes' 使用。我没有想出一种设计,可以让静态分析工具正确识别我的 'helper classes' 方法处理的类型。这是一个 MWE,说明了我 运行 遇到的(其中一个)问题:

我的库

from typing import Dict


class Thing:
    def shout(self):
        print(f"{self} says AAAAAAAAAaaaaaaaa")


class ContainerOfThings:
    def __init__(self):
        thing_cls = self._thing_cls = get_unique_subclass(Thing)
        self._things: Dict[str, thing_cls] = {}

    def add_thing(self, id_: str):
        self._things[id_] = self._thing_cls()

    def get_thing(self, id_: str):
        return self._things[id_]


def get_unique_subclass(cls):
    # this works but maybe there's a better way to do this?
    classes = cls.__subclasses__()
    if len(classes) == 0:
        return cls
    elif len(classes) == 1:
        return classes[0]
    elif len(classes) > 1:
        raise RuntimeError(
            "This class should only be subclassed once", cls, classes
        )

我希望用户使用它做什么

class BetterThing(Thing):
    def be_civilized(self):
        print(f"{self} says howdy!")

container = ContainerOfThings()
container.add_thing("some_id")
thingy = container.get_thing("some_id")
thingy.be_civilized()
thingy.do_something_invalid()  # here I would like mypy to detect that this will not work

此代码段不会向静态分析工具发出警报,因为 thingy 被检测为 Any,但在最后一行的 运行 时间失败,因为 do_something_invalid() 未定义。难道不能在这里提示 thingy 实际上是 BetterThing 的一个实例吗?

我目前的尝试:

尝试 1

ContainerOfThings._things 注释为 Dict[str, Thing] 而不是 Dict[str, thing_cls]

这通过了 mypy,但是 pycharm 将 thingy 检测为 Thing 的实例,因此抱怨“class 的未解析属性引用 'be_civilized' 'Thing'"

尝试 2

ContainerOfThings.get_thing()return值注释为Thing

不出所料,这会触发 pycharm 和 mypy 关于 Thing 没有 'be_civilized' 属性的错误。

尝试 3

使用 ThingType = TypeVar("ThingType", bound=Thing) 作为 ContainerOfThings.get_thing()

的 return 值

我相信(?)这就是 TypeVar 的目的,并且它有效,除了 mypy 然后需要 thingyBetterThing 注释的事实,连同 ContainerOfThings.get_thing() 的每个 return 值,这对于我的 'real' 库来说会非常麻烦。

有没有优雅的解决方案? get_unique_subclass() 玩静态分析是不是太肮脏了? typing_extensions.Protocol 有什么我想不出的聪明办法吗?

感谢您的建议。

基本上你需要 ContainerOfThings 才能通用:
https://mypy.readthedocs.io/en/stable/generics.html#defining-generic-classes

然后我认为 ContainerOfThings 明确说明它将生成的事物的类型,而不是 auto-magically 定位一些已定义的 sub-class 会更好。

我们可以将它们组合在一起以满足 mypy 的要求(我也希望 pycharm,虽然我还没有尝试过)...

from typing import Dict, Generic, Type, TypeVar


class Thing:
    def shout(self):
        print(f"{self} says AAAAAAAAAaaaaaaaa")


T = TypeVar('T', bound=Thing)


class ContainerOfThings(Generic[T]):
    def __init__(self, thing_cls: Type[T]):
        self._thing_cls = thing_cls
        self._things: Dict[str, T] = {}

    def add_thing(self, id_: str):
        self._things[id_] = self._thing_cls()

    def get_thing(self, id_: str) -> T:
        return self._things[id_]


class BetterThing(Thing):
    def be_civilized(self):
        print(f"{self} says howdy!")
        

container = ContainerOfThings(BetterThing)
container.add_thing("some_id")
thingy = container.get_thing("some_id")
thingy.be_civilized()  # OK
thingy.do_something_invalid()  # error: "BetterThing" has no attribute "do_something_invalid"