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.
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:
the function apply_decorator
is called, with argument f1
we are now inside apply_decorator
, which calls the decorator
function, passing the argument received (so f1
)
the function decorator
is decorated with decorator_for_decorator
, which means we now have to execute decorator_for_decorator
.
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.
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
f2
basically runs the code returned by func = decorator(func)
decorator
, which prints "Function successfully decorated"
and then executes whichever function has been passed in, in our case it's f1
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:
the function apply_decorator
is called, with argument f1
we are now inside apply_decorator
, which calls the decorator
function, passing the argument received (so f1
)
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.
we return without printing anything on screen.
What happens now when we call f2()
?
as before, calling f2
calls the function returned by func = decorator(func)
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.
We print on screen "Decorator successfully decorated"
We now run wrapper
inside decorator
, which prints "Function successfully decorated"
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