How to create a Python decorator that can be used either with or without parameters?

前端 未结 13 1637
死守一世寂寞
死守一世寂寞 2020-11-30 21:26

I\'d like to create a Python decorator that can be used either with parameters:

@redirect_output(\"somewhere.log\")
def foo():
    ....

or

13条回答
  •  野性不改
    2020-11-30 21:27

    Since no one mentioned this, there is also a solution utilizing callable class which I find more elegant, especially in cases where the decorator is complex and one may wish to split it to multiple methods(functions). This solution utilizes __new__ magic method to do essentially what others have pointed out. First detect how the decorator was used than adjust return appropriately.

    class decorator_with_arguments(object):
    
        def __new__(cls, decorated_function=None, **kwargs):
    
            self = super().__new__(cls)
            self._init(**kwargs)
    
            if not decorated_function:
                return self
            else:
                return self.__call__(decorated_function)
    
        def _init(self, arg1="default", arg2="default", arg3="default"):
            self.arg1 = arg1
            self.arg2 = arg2
            self.arg3 = arg3
    
        def __call__(self, decorated_function):
    
            def wrapped_f(*args):
                print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
                print("decorated_function arguments:", *args)
                decorated_function(*args)
    
            return wrapped_f
    
    @decorator_with_arguments(arg1=5)
    def sayHello(a1, a2, a3, a4):
        print('sayHello arguments:', a1, a2, a3, a4)
    
    @decorator_with_arguments()
    def sayHello(a1, a2, a3, a4):
        print('sayHello arguments:', a1, a2, a3, a4)
    
    @decorator_with_arguments
    def sayHello(a1, a2, a3, a4):
        print('sayHello arguments:', a1, a2, a3, a4)
    

    If the decorator is used with arguments, than this equals:

    result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)
    

    One can see that the arguments arg1 are correctly passed to the constructor and the decorated function is passed to __call__

    But if the decorator is used without arguments, than this equals:

    result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)
    

    You see that in this case the decorated function is passed directly to the constructor and call to __call__ is entirely omitted. That is why we need to employ logic to take care of this case in __new__ magic method.

    Why can't we use __init__ instead of __new__? The reason is simple: python prohibits returning any other values than None from __init__

    WARNING

    This approcach has one side effect. It will not preserve function signature!

提交回复
热议问题