Search code examples
pythondecoratorpython-decoratorsparentheses

Calling Decorator without Parentheses (without changing decorator definition)


Suppose I have the following decorator. (To repeat a function n times)

def repeat(num_times=4):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

Now, it does have a default value of 4, however, even if I want to call it with default value, I still have to call it as follows

@repeat()
def my_function():
    print("hello")

instead of

@repeat
def my_function():
    print("hello")

Now, I can change the definition of my decorator to

def repeat(_func=None, *, num_times=2):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat

    if _func is None:
        return decorator_repeat
    else:
        return decorator_repeat(_func)

And enable the functionality to call it without arguments if I want to.

However, can this be achieved without changing the code of the decorator, but by defining another decorator?

i.e. I want to define a decorator enable_direct so that I can just add @enable_direct to my decorator definition and have the same effect. (i.e. as follows)

@enable_direct
def repeat(num_times=4):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

Note:

I am aware of the solution mentioned in How to create a Python decorator that can be used either with or without parameters?

The definitions in that question have a different signature, and if one is starting afresh, one can follow that pattern. However, say I have 20-30 such decorator definitions (3 level nested). I want all of these to be enabled to be called without parentheses. The def repeat statement does not have a function argument. The functions in that question have a 2 level nesting, while mine has 3 level. I wanted to ask if it is possible with such decorator definitions (which are meant to be called with parentheses) without changing the function definition. The accepted answer there has a different signature, and thus does not meat requirement in this question.

Note 2: I did not ask this question before trying out the definition of double wrap given there. Calling it without parentheses returns another function (if the signature of function is as described).


Solution

  • Here you are:

    import functools
    
    
    def enable_direct(decorator):
        @functools.wraps(decorator)
        def wrapper(*args, **kwargs):
            f = args[0]
            if callable(f):
                return decorator()(f)  # pass the function to be decorated
            else:
                return decorator(*args, **kwargs)  # pass the specified params
        return wrapper
    
    
    @enable_direct
    def repeat(num_times=4):
        def decorator_repeat(func):
            @functools.wraps(func)
            def wrapper_repeat(*args, **kwargs):
                for _ in range(num_times):
                    value = func(*args, **kwargs)
                return value
            return wrapper_repeat
        return decorator_repeat
    
    
    @repeat
    def my_func(name):
        print(name)
    
    @repeat(2)
    def my_func2(name):
        print(name)
    
    
    print(my_func)
    print(my_func2)
    
    my_func("Gino")
    my_func2("Mario")
    

    Which produces

    <function my_func at 0x7f629f091b70>
    <function my_func2 at 0x7f629f091bf8>
    Gino
    Gino
    Gino
    Gino
    Mario
    Mario