有没有办法在继承期间持久化装饰器?

Posted

技术标签:

【中文标题】有没有办法在继承期间持久化装饰器?【英文标题】:Is there a way to persist decorators during inheritance? 【发布时间】:2020-04-14 23:53:08 【问题描述】:

我正在尝试使用未实现的方法编写一个抽象类,这将强制继承的子级在重写方法(在装饰器中定义)时返回特定类型的值。

当我使用下面显示的代码时,子方法不会调用装饰器。我认为这是因为该方法被覆盖,这很有意义。我的问题基本上是这样的: 有没有办法通过方法覆盖使装饰器持久化?

我不反对使用装饰器以外的其他东西,但这是一个很快就浮现在脑海中的解决方案,我很想知道是否有任何方法可以让它发挥作用。

如果使用装饰器是正确且可能的选择,它将如下所示:

def decorator(returntype):
    def real_decorator(function):
        def wrapper(*args, **kwargs):
            result = function(*args, **kwargs)
            if not type(result) == returntype:
                raise TypeError("Method must return 0".format(returntype))
            else:
                return result
        return wrapper
    return real_decorator

我需要我的父类看起来与这个相似:

class Parent(ABC):
    @decorator(int)
    @abstractmethod
    def aye(self, a):
        raise NotImplementedError

子类会做这样的事情:

class Child(Parent):
    def aye(self, a):
        return a

如果需要,我非常乐意更好地澄清我的问题,并感谢所有花时间提前阅读此问题的人!

【问题讨论】:

是的。例如,对于一个元类,您可以在其中更改所选方法以应用装饰器。请参阅 here 和 here 和 here 和 here 以获得好的想法和示例。 如果你真的有很好的用例,你也应该metathink 您还要求做两件不同的事情。正如@mgc 提到的那样,静态类型检查可能就足够了。强制签名对我来说似乎总是反python,但在某些情况下可能需要/有用。没有质疑。 如果返回类型强制是你唯一的目标,你能不使用更简单的东西吗?说def aye(self, a): res = self._aye(a) assert isinstance(res, int) return res,其中_aye 是子类的实际方法,aye 是它的公共类型检查调用器? 感谢您关注我的问题@progmatico!由于此类消费者的某些限制,我必须确保这些方法以错误的返回类型失败。 @mgc 的解决方案可以工作,因为如果 mypy 没有通过,我可能会使 CI/CD 进程失败,但我绝对必须强制执行返回类型:) 【参考方案1】:

我不确定你是否可以按照你想要的方式保持装饰器的效果,但你仍然可以在 Parent 类中装饰一个包装函数,它不会是一个 abstractmethod 并让子类实现像这样的包装函数:

from abc import ABC, abstractmethod

def decorator(returntype):
    def real_decorator(function):
        def wrapper(*args, **kwargs):
            result = function(*args, **kwargs)
            if not type(result) == returntype:
                raise TypeError("Method must return 0".format(returntype))
            else:
                return result
        return wrapper
    return real_decorator

class Parent(ABC):
    @decorator(int)
    def aye(self, a):
        return self.impl_aye(a)

    @abstractmethod
    def impl_aye(self, a):
        raise NotImplementedError


class Child(Parent):
    def impl_aye(self, a):
        return a

还有一些解决方案可以保护 aye 方法不受 Parent 类的影响,如果需要,请参阅 this answer 示例。

否则,如果您想使用 type hints 并使用 mypy(Python 的可选静态类型检查器)检查您的代码,如果您尝试使用以下命令实现子类,则会收到错误消息返回类型与其父类不兼容:

from abc import ABC, abstractmethod

class Parent(ABC):
    @abstractmethod
    def aye(self, a) -> int:
        raise NotImplementedError

class Child(Parent):
    def aye(self, a) -> str :
        return a

mypy 的输出:

a.py:9: error: Return type "str" of "aye" incompatible with return type "int" in supertype "Parent"
Found 1 error in 1 file (checked 1 source file)

【讨论】:

这是一个很好的解决方案,如果我可以使用它使我的 CI/CD 流程失败,我可能会选择这个答案。非常感谢! 不客气!如果我很好地理解了您的问题,我想您可以使您的 CI 过程因我的两个提议而失败(尽管我对我的第一个提议并不感到非常自豪,因为它为开发人员创建了间接性,因为您必须实现 @ 987654332@(或任何其他名称)当呼叫者呼叫aye,所以不要忘记@progrmatico第一条评论中也有很好的建议!)【参考方案2】:

如果你只是想要强制返回类型,这是我的非装饰器建议(最初没有放入,因为我不喜欢你-不要-想要在 SO 上做这个“答案”)。

class Parent:

    def aye(self, a):
        res = self._aye(a)
        if not isinstance(res, int):
            raise TypeError("result should be an int")
        return res

    def _aye(self, a):
        raise NotImplementedError()

class Child(Parent):

    def _aye(self, a):
        return 1

【讨论】:

引发异常而不是断言会更好,因为断言可以被禁用。 同意并更改。【参考方案3】:

以下是使用元类执行此操作的方法。在 Python 3.8 上测试。应该在 3.6 及更高版本上按原样工作。诚然,这有点复杂,使用另一种技术可能会更好。

from abc import ABCMeta, abstractmethod
from functools import wraps
from inspect import isfunction


class InheritedDecoratorMeta(ABCMeta):

    def __init__(cls, name, bases, attrs):
        for name, attr in attrs.items():
            for base in bases:
                base_attr = base.__dict__.get(name)
                if isfunction(base_attr):
                    inherited_decorator = getattr(base_attr, 'inherited_decorator', None)
                    if inherited_decorator:
                        setattr(cls, name, inherited_decorator()(attr))
                        break


def inherited_decorator(decorator, result_callback):
    def inner_decorator(method):
        method.inherited_decorator = lambda: inherited_decorator(decorator, result_callback)
        @wraps(method)
        def wrapper(*args, **kwargs):
            result = method(*args, **kwargs)
            return result_callback(method, result, args, kwargs)
        return wrapper
    return inner_decorator


def returns(type_):
    if not isinstance(type_, type) and type_ is not None:
        raise TypeError(f'Expected type or None; got type_')
    def callback(method, result, args, kwargs):
        result_type = type(result)
        if type_ is None:
            if result is not None:
                raise TypeError(f'Expected method method to return None; got result_type')
        elif not isinstance(result, type_):
            raise TypeError(f'Expected method method to return type_; got result_type')
        return result
    return inherited_decorator(returns, callback)


class MyBaseClass(metaclass=InheritedDecoratorMeta):

    @returns(int)
    @abstractmethod
    def aye(self, a):
        raise NotImplementedError

    @returns(None)
    @abstractmethod
    def bye(self, b):
        raise NotImplementedError


class MyClass(MyBaseClass):

    def aye(self, a):
        return a

    def bye(self, b):
        return b

    @returns(str)
    def cye(self, c):
        return c


if __name__ == '__main__':
    instance = MyClass()

    instance.aye(1)
    try:
        instance.aye('1')
    except TypeError as exc:
        print(exc)

    instance.bye(None)
    try:
        instance.bye(1)
    except TypeError as exc:
        print(exc)

    instance.cye('string')
    try:
        instance.cye(1)
    except TypeError as exc:
        print(exc)

【讨论】:

以上是关于有没有办法在继承期间持久化装饰器?的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法通过嵌套提供者获取函数装饰器元数据?

装饰器与继承

如何在类中声明装饰器,以装饰已经装饰的继承方法?

Python:如何在单元(鼻子)测试期间忽略装饰器?

如果字段可以为空,则验证期间的装饰器仍然有效

装饰器