Search code examples
pythonpython-3.xdecoratorpython-decorators

Decorating a decorator using a function - why does the following not work?


I am struggling to understand the following behaviour of decorated decorators - could someone help me please?

Basically, I have a decorator_for_decorator function that decorates a decorator:

def decorator_for_decorator(func):
    def wrapper(*args):
        print('Decorator successfully decorated')
        return func(*args)
    return wrapper

When I decorate my decorator like this:

# this doesn't work
@decorator_for_decorator
def decorator(func):
    def wrapper(*args):
        print('Function successfully decorated')
        return func(*args)
    return wrapper

def apply_decorator(func):
    func = decorator(func)
    return func

def f1():
    print('hello')

f2 = apply_decorator(f1)

f2()

The call to f2() returns:

Function successfully decorated
hello

Indicating that the decorator_for_decorator did not decorate my decorator. However, if I instead decorate my decorator like this:

# this works
def decorator(func):
    @decorator_for_decorator
    def wrapper(*args):
        print('Function successfully decorated')
        return func(*args)
    return wrapper

f2 = apply_decorator(f1)

f2()

Calling f2() returns:

Decorator successfully decorated
Function successfully decorated
hello

Why is this the case? I would expect both ways to decorate the decorator to work but it seems like only the latter works.


Solution

  • I could be wrong, but I think the confusion comes from when a decorator runs.

    Let's break down this example, the one you marked as this doesn't work

    def decorator_for_decorator(func):
        def wrapper(*args):
            print('Decorator successfully decorated')
            return func(*args)
        return wrapper
    
    @decorator_for_decorator
    def decorator(func):
        def wrapper(*args):
            print('Function successfully decorated')
            return func(*args)
        return wrapper
    
    def apply_decorator(func):
        func = decorator(func)
        return func
    
    def f1():
        print('hello')
    

    When you call:

    f2 = apply_decorator(f1)
    

    The execution path is:

    1. the function apply_decorator is called, with argument f1

    2. we are now inside apply_decorator, which calls the decorator function, passing the argument received (so f1)

    3. the function decorator is decorated with decorator_for_decorator, which means we now have to execute decorator_for_decorator.

    4. we are now inside decorator_for_decorator and we are executing it (since it has been called from being a decorator). The output "Decorator successfully decorated" is printed on screen.

    5. we continue execution going back inside decorator, which returns a function. Only when this returned function is called it will execute, printing the line Function successfully decorated, but note as we are never executing the function here, since we never call the returned function f2 from f2 = apply_decorator(f1)

    The code execution now stops. We have a function f2 ready to be used.

    What happens when we call f2()? Let's see

    1. calling f2 basically runs the code returned by func = decorator(func)
    2. we run the function returned by decorator, which prints "Function successfully decorated" and then executes whichever function has been passed in, in our case it's f1
    3. we execute f1, printing "hello" and we finish.

    Now, if you call f2() again, you will re-do the exact same execution as above, so it will print "Function successfully decorated" and then "hello".

    You get "Decorator successfully decorated" only once, after calling f2 = apply_decorator(f1), because this is when the decorator applied to def decorator(func): is supposed to run.

    Now, let's look at the other example:

    def decorator_for_decorator(func):
        def wrapper(*args):
            print('Decorator successfully decorated')
            return func(*args)
        return wrapper
    
    def decorator(func):
        @decorator_for_decorator
        def wrapper(*args):
            print('Function successfully decorated')
            return func(*args)
        return wrapper
    
    def apply_decorator(func):
        func = decorator(func)
        return func
    
    def f1():
        print('hello')
    
    f2 = apply_decorator(f1)
    

    The execution path is:

    1. the function apply_decorator is called, with argument f1

    2. we are now inside apply_decorator, which calls the decorator function, passing the argument received (so f1)

    3. the function decorator is NOT decorated with decorator_for_decorator, but it's the function wrapper inside that it is decorated and the decorator runs only when the decorated function runs. In this case, it will run only when wrapper runs.

    4. we return without printing anything on screen.

    What happens now when we call f2()?

    1. as before, calling f2 calls the function returned by func = decorator(func)

    2. we run the function returned by decorator, which is wrapper. Now, this function is decorated by @decorator_for_decorator, so we have to run `decorator_for_decorator first.

    3. We print on screen "Decorator successfully decorated"

    4. We now run wrapper inside decorator, which prints "Function successfully decorated"

    5. We finally execute f1 printing "hello"

    When you call f2() again, you will go through the same code path, printing "Decorator successfully decorated", then "Function successfully decorated" and lastly "hello"

    Hopefully this step-by-step explanation helps you clarifying your doubts about decorators