使用类作为装饰器和装饰器工厂的操作方法

3

考虑以下装饰器函数,它可以返回已经被装饰的函数,也可以返回带参数的装饰函数:

from functools import wraps, partial, update_wrapper
from inspect import signature

def wrapit(func=None, *, verb='calling'):
    if func is None:  # return a decoratOR
        return partial(wrapit, verb=verb)
    else:  # return a decoratED
        @wraps(func)
        def _func(*args, **kwargs):
            print(f'{verb} {func.__name__} with {args} and {kwargs}')
            return func(*args, **kwargs)
        return _func

示例:

>>> f = lambda x, y=1: x + y
>>> ff = wrapit(verb='launching')(f)
>>> assert ff(10) == 11
launching <lambda> with (10,) and {}
>>> assert signature(ff) == signature(f)
>>>
>>> # but can also use it as a "decorator factory"
>>> @wrapit(verb='calling')
... def f(x, y=1):
...     return x + y
...
>>> assert ff(10) == 11
launching <lambda> with (10,) and {}
>>> assert signature(ff) == signature(f)

表单类的外观可能如下所示:

class Wrapit:
    def __init__(self, func, verb='calling'):
        self.func, self.verb = func, verb
        update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        print(f'{self.verb} {self.func.__name__} with {args} and {kwargs}')
        return self.func(*args, **kwargs)

但是我们如何让类能够以函数式实现的“装饰器工厂”模式运作(由if func is None: return partial...实现)?

我们如何将这个技巧整合到一个装饰器类中?


1
你可以在__new__中使用相同的技巧。 - juanpa.arrivillaga
不确定你的意思 @juanpa.arrivillaga。__new__ 只接受一个类作为参数,所以我怎么可以基于 func is None 条件来决定我的操作呢? - thorwhalen
2个回答

2

正如评论中所建议的那样,您可以使用__new__方法来实现:

class Wrapit:
    def __new__(cls, func=None, *, verb='calling'):
        if func is None:
            return partial(cls,verb=verb)
        self = super().__new__(cls)
        self.func, self.verb = func, verb
        update_wrapper(self, func)
        return self

    def __call__(self, *args, **kwargs):
        print(f'{self.verb} {self.func.__name__} with {args} and {kwargs}')
        return self.func(*args, **kwargs)
< p > __new__方法在尝试实例化一个类时被调用,该方法的返回值被用作尝试实例化的结果,即使它不是该类的实例!< /p >

接受你的答案。谢谢。请注意,我稍微编辑了你的答案(将 object.__new__(cls) 替换为 super(Wrapit, cls).__new__(cls) 等)。如果你不同意,请恢复原样。 - thorwhalen
@thorwhalen 我接受了那个更改,但拒绝将其拆分为两种方法的更改,我认为这并不更清晰(我选择了“拒绝并编辑”,因为我从来不确定何时使用它或“改进编辑”当我想要接受一半的编辑时) - pppery

1
我接受了@pppery的答案,因为它是正确的。我想通过展示如何在父类中编写逻辑来扩展答案,从而实现更多的重复利用。这需要将@pppery的逻辑分别编写到__new____init__方法中。
from functools import update_wrapper, partial

class Decorator:
    def __new__(cls, func=None, **kwargs):
        if func is None:
            self = partial(cls, **kwargs)
        else:
            self = super().__new__(cls)
        return update_wrapper(self, func)

    def __init__(self, func=None, **kwargs):
        self.func = func
        for attr_name, attr_val in kwargs.items():
            setattr(self, attr_name, attr_val)

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

class Wrapit(Decorator):
    def __new__(cls, func=None, *, verb='calling'):
        return super().__new__(cls, func, verb=verb)

    def __call__(self, *args, **kwargs):
        print(f'{self.verb} {self.func.__name__} with {args} and {kwargs}')
        return super().__call__(*args, **kwargs)

class AnotherOne(Decorator):
    def __new__(cls, func=None, *, postproc=lambda x: x):
        return super().__new__(cls, func, postproc=postproc)

    def __call__(self, *args, **kwargs):
        return self.postproc(super().__call__(*args, **kwargs))

演示:

>>> f = lambda x, y=1: x + y
>>> 
>>> ff = Wrapit(f, verb='launching')
>>> assert ff(10) == 11
launching <lambda> with (10,) and {}
>>> assert signature(ff)  == signature(f) 
>>> 
>>> fff = AnotherOne(postproc=str)(f)  # doing it the decorator factory way
>>> assert fff(10) == str(11)
>>> assert signature(fff)  == signature(f)

谢谢你,@pppery。不过我必须说,这不是我理想的版本:子类__new__方法仅在提供子类装饰器工厂的签名时才存在,但除此之外并没有做任何实际工作。我一直在寻找简化这个方面的方法。我会在另一个问题中发布它,并引导你去那里看。 - thorwhalen
@pppery:请参见 https://stackoverflow.com/questions/62399050/a-lean-interface-for-making-python-decorator-classes - thorwhalen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接