我正在编写一个Python库,通过导入和(可选)子类化它提供的一些“帮助器类”来使用。我未能提出一种设计,可以正确地让静态分析工具正确识别我的“辅助类”方法处理的类型。这是一个 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
的一个实例吗?
到目前为止我的尝试:
将
ContainerOfThings._things
注释为 Dict[str, Thing]
而不是 Dict[str, thing_cls]
这通过了 mypy,但 pycharm 检测到
thingy
作为 Thing
的实例,因此抱怨“类 'Thing' 的未解析属性引用 'be_civilized'”
将
ContainerOfThings.get_thing()
返回值注释为 Thing
不足为奇的是,这会触发 pycharm 和 mypy 中关于
Thing
没有“be_civilized”属性的错误。
使用
ThingType = TypeVar("ThingType", bound=Thing)
作为 ContainerOfThings.get_thing()
的返回值
我相信(?)这就是
TypeVar
的用途,并且它有效,除了 mypy 然后需要用 thingy
注释 BetterThing
以及 ContainerOfThings.get_thing()
的每个返回值,这对于我的“真正的”库来说会非常麻烦。
有一个优雅的解决方案吗?
get_unique_subclass()
是不是太肮脏了,无法很好地进行静态分析?有什么聪明的办法可以用typing_extensions.Protocol
来做但我想不出吗?
感谢您的建议。
基本上你需要
ContainerOfThings
才能通用:然后我认为
ContainerOfThings
最好明确它将生成的事物的类型,而不是自动神奇地定位已定义的某些子类。
我们可以用一种满足 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"