Decorator is a function that returns another function. the goal of decorators is to add logic to existing functions. common use cases could be log the output, check the permissions before running the function and …
Take the following code as an example
1
2
3
4
5
6
7
8
9
10
11
| def log(func):
def wrapper(*args, **kwargs):
print(f"executing {func} with arguments {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
def test():
print("hello")
test = log(test)
|
The log
function is a decorator. it takes a function as an argument and returns another function. the returned function is the one that will be executed when we call test
. the wrapper
function is the one that will be executed when we call test
. it logs the arguments and the function name before executing the function.
The sugar syntax is
1
2
3
| @log
def test():
print("hello")
|
What if we had multiple decorators?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| def decorator1(func):
print("inside decorator1")
def wrapper(*args, **kwargs):
print(f"inside decorator1 wrappper")
print(f"executing {func}")
return func(*args, **kwargs)
return wrapper
def decorator2(func):
print("inside decorator2")
def wrapper(*args, **kwargs):
print(f"inside decorator2 wrappper")
print(f"executing {func}")
return func(*args, **kwargs)
return wrapper
@decorator1
@decorator2
def test():
print("hello")
|
What’s the output of the code? the above code is just definition of functions. it doesn’t run any function. with that being said, let’s uncompress the decorator syntax
1
2
3
4
| def test():
print("hello")
test = decorator1(decorator2(test))
|
So the output is
inside decorator2
inside decorator1
If we add the following line to the code
The output would be
inside decorator2
inside decorator1
inside decorator1 wrappper
executing <function decorator2.<locals>.wrapper at 0x7f48ee8113f0>
inside decorator2 wrappper
executing <function test at 0x7f48ee811360>
hello
Decorators with arguments
A decorator without argument was like
We want to add arguments to the decorator. how can we rewrite it?
1
| test = decorator(*decorator_args, **decorator_kwargs)(test)
|
To use the sugar syntaxt
1
2
3
4
5
6
| def decorator(*decorator_args, **decorator_kwargs)
def inner(func):
def wrapper(*args, **kwarg):
return func(*args, **kwargs)
return wrapper
return inner
|
You can compare it with the decorator without argument
1
2
3
4
| def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
|
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| def decorator(*decorator_args, **decorator_kwargs):
print(f"decorator arguments {decorator_args} {decorator_kwargs}")
def inner(func):
def wrapper(*args, **kwargs):
print(f"inside wrappper")
print(f"executing {func} with arguments {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
return inner
@decorator(1, 2)
def test(*args, **kwargs):
"""Test function"""
print(f"function arguments {args} {kwargs}")
print("hello")
test(3, 4)
|
Output
1
2
3
4
5
| decorator arguments (1, 2) {}
inside wrappper
executing <function test at 0x7f1338b0d360> with arguments (3, 4) {}
function arguments (3, 4) {}
hello
|
The actual function which is test
is replaced by wrapper
which as a consequence replaces the name and docstring of it
print(test.__name__)
print(test.__doc__)
Output
wrapped
None
To preserve those attribute, we can use wraps
decorator on the wrapped
function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from functools import wraps
def decorator(*decorator_args, **decorator_kwargs):
print(f"decorator arguments {decorator_args} {decorator_kwargs}")
def inner(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"inside wrappper")
print(f"executing {func} with arguments {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
return inner
@decorator(1, 2)
def test(*args, **kwargs):
"""Test function"""
print(f"function arguments {args} {kwargs}")
print("hello")
|
And the output of the preivous code would be
test
Test function
We saw some example of function decorators. what if we want to use them on a class or instance method? can you guess what we need to change?
The difference is that a method has by default one arguemnet which is the class instance or class itself. so basically it’s the same as function decorator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| def method_decorator(func):
print("inside decorator")
def wrapper(self, *args, **kwargs):
print(f"inside wrappper")
print(f"executing {func} with arguments {args} {kwargs}")
return func(self)
return wrapper
class A:
@method_decorator
def test(self):
print("hello")
A().test()
|
Output
inside decorator
inside wrappper
executing <function A.test at 0x7f8d1cb0d360> with arguments () {}
hello