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



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

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

我制作了以下子类工厂,作为本地类的类装饰器和导入类的函数。

但到目前为止,我未能正确键入hint的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

我最近一次打字尝试的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"]
  1. 我不明白为什么cls作为一种类型无效,即使在阅读之后也是如此https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs类型别名。

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

PS1.我也尝试过一个动态添加b的装饰器,但仍然无法键入…

PS2.…根据@DaniilFajnberg的回答,装饰器使用了一个mixin,但仍然失败。


参考文献:

  1. functools.wraps(cls, update=())https://stackoverflow.com/a/65470430/17676984

(类型(变量作为基类

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

我读了一些关于这两个错误的文章:

变量"cls";作为类型无效/基类"无效";cls";

mypy似乎有一个问题,它已经开放了很长时间。似乎还没有解决办法。

据我所知,问题在于,无论你如何注释它,函数参数cls都将始终是一个类型变量,并且作为基类被认为是无效的。其理由显然是,无法确保该变量的值不会在某个地方被覆盖。

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

A = type('A', (object,), {})

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"])

输出:True

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


与外国班混合

如果您正在处理外来类,您仍然可以使用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__=}')

输出:

True
MixedForeign.__name__='Foreign' MixedForeign.__doc__='Foreign class documentation'

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

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


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

最新更新