类型提示返回子类的类装饰器

ljm*_*jmc 5 python python-3.x python-decorators python-typing

我有一组不相关的类(一些是导入的),它们都有一个a类型为 的公共属性(或属性) dict[str, Any]

其中akey 下应该有另一个字典"b",我想将其作为属性公开在任何这些类上b以简化inst.a.get("b", {})[some_key]inst.b[some_key]

我已将以下子类工厂用作本地类的类装饰器和导入类的函数。

但到目前为止,我未能cls正确键入提示其参数并返回值。

from functools import wraps

def access_b(cls):
    @wraps(cls, updated=())
    class Wrapper(cls):
        @property
        def b(self) -> dict[str, bool]:
            return self.a.get("b", {})
    return Wrapper
Run Code Online (Sandbox Code Playgroud)

我最近一次打字尝试的 MRE(有mypy 0.971错误):

from functools import wraps
from typing import Any, Protocol, TypeVar

class AProtocol(Protocol):
    a: dict[str, Any]

class BProtocol(AProtocol, Protocol):
    b: dict[str, bool]

T_a = TypeVar("T_a", bound=AProtocol)
T_b = TypeVar("T_b", bound=BProtocol)

def access_b(cls: type[T_a]) -> type[T_b]:
    @wraps(cls, updated=())
    class Wrapper(cls):  # Variable "cls" is not valid as a type & Invalid base class "cls"
        @property
        def b(self) -> dict[str, bool]:
            return self.a.get("b", {})
    return Wrapper

@access_b
class Demo1:
    """Local class."""
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()

demo1 = Demo1({"b": {"allow_X": True}})
demo1.b["allow_X"]  # "Demo1" has no attribute "b"

class Demo2:
    """Consider me an imported class."""
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()

demo2 = access_b(Demo2)({"b": {"allow_X": True}})  # Cannot instantiate type "Type[<nothing>]"
demo2.b["allow_X"]
Run Code Online (Sandbox Code Playgroud)
  1. 我不明白为什么cls作为一种类型无效,即使在阅读https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases后也是如此。

  2. 我知道我可能不应该返回 Protocol (我怀疑这是 的来源Type[<nothing>]),但我不知道如何指定“返回带有扩展名的原始类型”。

PS1。我也尝试过使用动态添加 b 的装饰器,但仍然无法输入它......

PS2。...并且使用按照 @DaniilFajnberg 的答案使用 mixin 的装饰器,仍然失败。


参考:

  1. functools.wraps(cls, update=())来自/sf/answers/4582930131/

Dan*_*erg 2

(类型)变量作为基类?

这实际上是一个非常有趣的问题,我很好奇其他人提出了什么解决方案。

我读了一些关于这两个错误的内容:

变量“cls”作为类型无效/基类“cls”无效

这里似乎有一个问题已经mypy存在很长时间了。似乎还没有解决方法。

据我了解,问题在于,无论您如何注释它,函数参数cls始终是类型变量,并且作为基类被认为是无效的。显然,原因是无法确保该变量的值不会在某处被覆盖。

老实说,我不太理解其中的复杂性,但对我来说真的很奇怪,mypy似乎对待A通过class A: ...不同于变量定义的类Type[A],因为前者本质上应该只是语法糖:

A = type('A', (object,), {})
Run Code Online (Sandbox Code Playgroud)

问题跟踪器中也有相关讨论mypy。再次,希望有人能对此有所启发。


添加便利属性

无论如何,从你的例子中我了解到你不是在处理外国类,而是你自己定义它们。如果是这种情况,混合将是最简单的解决方案:

from typing import Any, Protocol


class AProtocol(Protocol):
    a: dict[str, Any]


class MixinAccessB:
    @property
    def b(self: AProtocol) -> dict[str, bool]:
        return self.a.get("b", {})


class SomeBase:
    ...


class OwnClass(MixinAccessB, SomeBase):
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()


demo1 = OwnClass({"b": {"allow_X": True}})
print(demo1.b["allow_X"])
Run Code Online (Sandbox Code Playgroud)

输出:True

模式下没有mypy问题--strict


与外国班级混音

如果您正在处理外部类,您仍然可以使用 Mix-in,然后functools.update_wrapper像这样使用:

from functools import update_wrapper
from typing import Any, Protocol


class AProtocol(Protocol):
    a: dict[str, Any]


class MixinAccessB:
    """My mixin"""
    @property
    def b(self: AProtocol) -> dict[str, bool]:
        return self.a.get("b", {})


class Foreign:
    """Foreign class documentation"""
    def __init__(self, a: dict[str, Any]):
        self.a = a.copy()


class MixedForeign(MixinAccessB, Foreign):
    """foo"""
    pass


update_wrapper(MixedForeign, Foreign, updated=())


demo2 = MixedForeign({"b": {"allow_X": True}})
print(demo2.b["allow_X"])
print(f'{MixedForeign.__name__=} {MixedForeign.__doc__=}')
Run Code Online (Sandbox Code Playgroud)

输出:

True
MixedForeign.__name__='Foreign' MixedForeign.__doc__='Foreign class documentation'
Run Code Online (Sandbox Code Playgroud)

模式下也没有mypy问题--strict

请注意,您仍然需要AProtocol明确self该属性中的任何内容都遵循该协议,即具有a类型为 的属性dict[str, Any]


我希望我正确理解了您的要求,这至少为您的特定情况提供了解决方案,即使我无法就类型变量问题向您提供启发。