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

发布于 2025-01-31 17:14:19 字数 2403 浏览 2 评论 0原文

我正在编写一个python库,该库是通过导入和(可选的)子类别提供的一些“助手类”所提供的。我无法提出一个可以正确允许静态分析工具正确识别我的“助手类”方法类型的设计。这是我遇到的问题(其中之一)的MWE:

我的lib

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

摘要没有警告静态分析工具,因为将事物检测到abor,但在失败最后一行的运行时间是因为do_something_invalid()尚未定义。难道是否可以提示Thingy实际上是Bettering的实例?

到目前为止,我的尝试是:

尝试1

Annotate contaerofthings._things AS dict [str,thing,thing]而不是dict> dict [str,thing,thing_cls]

这通过mypy be_civilized'for class'thing'thing''

的实例,因此抱怨“未解决的属性参考'

,但Pycharm将Thing作为thing thing ()返回值为thick

不足为奇的是,这会触发pycharm和mypy的错误,涉及thick没有“ be_civilized”属性。

尝试3

使用thingType = typeVar(“ ThingType”,Bound = Thing)作为containerOfthings.get_thing()我相信(?)的返回值

,这是什么> TypeVar是为了使用Thing的事实,并使用Betterthing,以及<的每个返回值,代码> containerofthings.get_thing(),我的“真实”库将很麻烦。

有一个优雅的解决方案吗? get_unique_subclass()太肮脏了,可以通过静态分析播放一个好的技巧吗?我无法提出的typing_extensions.protocol

感谢您的建议。

I am writing a python library used by importing and (optionally) sub-classing some of the 'helper classes' it provides. I fail to come up with a design that would properly let static analysis tools properly recognise the types that my 'helper classes' methods deal with. Here's a MWE illustrating (one of) the issues I run into:

My lib

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
        )

What I expect users to do with it

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

This snippet does not alarm static analysis tools, because thingy is detected as Any, but fails at runtime on the last line because do_something_invalid() is not defined. Isn't it possible to give hints that thingy is in fact an instance of BetterThing here?

My attempts so far:

Attempt 1

Annotate ContainerOfThings._things as Dict[str, Thing] instead of Dict[str, thing_cls]

This passes mypy, but pycharm detects thingy as an instance of Thing and thus complains about "Unresolved attribute reference 'be_civilized' for class 'Thing'"

Attempt 2

Annotate ContainerOfThings.get_thing() return value as Thing

Less surprisingly, this triggers errors from both pycharm and mypy about Thing not having the 'be_civilized' attribute.

Attempt 3

Use ThingType = TypeVar("ThingType", bound=Thing) as return value for ContainerOfThings.get_thing()

I believe (?) that this is what TypeVar is intended for, and it works, except for the fact that mypy then requires thingy to be be annotated with BetterThing, along with every return value of ContainerOfThings.get_thing(), which will be quite cumbersome with my 'real' library.

Is there an elegant solution for this? Is get_unique_subclass() too dirty a trick to play nice with static analysis? Is there something clever to do with typing_extensions.Protocol that I could not come up with?

Thanks for your suggestions.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

别把无礼当个性 2025-02-07 17:14:19

基本上,您需要contarerofthings是通用的:
https://mypy.readthedocs.io.iredocs.io/en/stable/ generics.html#定义基因级

,然后我认为contaerofthings可以明确地对其生成的东西的类型显式而不是自动找到某些子sub,这会更好 - 已定义的类。

我们可以将其放在一起以满足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"

Basically you need ContainerOfThings to be generic:
https://mypy.readthedocs.io/en/stable/generics.html#defining-generic-classes

And then I think it would be better for ContainerOfThings to be explicit about the type of thing that it will generate instead of auto-magically locating some sub-class that has been defined.

We can put this together in a way that will satisfy mypy (and I would expect pycharm too, though I haven't tried it)...

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"
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文