A decorator is a callable that takes a callable as input and returns a callable. This is the general definition of a decorator. The callable in this definition can be a function or a class. In our initial discussion, we will talk about decorator functions that are used to decorate functions; we will talk about classes later on. So, for the time being, we can think of a decorator as a function that takes a function as input and returns a function.
Decorators are used to add some functionality to a function. They allow you to execute some extra code before or after the execution of a function. This extra work is done without making any changes to the source code of the function. So, by using a decorator we can extend the behavior of a function without actually modifying its code.
A decorator is a function that takes another function as an argument, decorates it with the extra functionality and gives you a decorated function as the result.
def func1(): print('Hello world') print('Welcome to Python') def my_decorator(fn): def wrapper(): print('Hi … Starting execution') fn() print('Bye … finished executing\n') return wrapper decorated_func1 = my_decorator(func1)
@my_decorator def func3(): print('Learning decorators')
The function func3 is decorated using the automated decoration syntax and so now we do not need to write the statement func3 = my_decorator(func3). The @ syntax automates this reassignment of the function name.
Decorator Example: Checking return values
Decorator Example: Checking argument values
Preserving metadata of a function after decoration
If we do not want the original function to lose its name, documentation and other attributes, even after decoration, then we can use the wraps function from the functools module. This wraps function is a decorator that copies the introspection details of a function to another function. We will apply this decorator to the wrapper function.
from functools import wraps def trace(fn): @wraps(fn) def wrapper(*args, **kwargs): print(f'{fn.__name__} called') print(f'args : {args} kwargs : {kwargs}' ) result = fn(*args, **kwargs) print(f'Return value : {result}\n') return result return wrapper
This wraps decorator is little different, it is a decorator that takes an argument. We will see such decorators in the next section. We send function fn as argument to this wrapper function. So now all the important metadata of fn will be copied in the wrapper function.
General template for writing a decorator
from functools import wraps def decorator_name(fn): @wraps(fn) def wrapper(*args, **kwargs): #Place code that has to be executed before function call result = fn(*args, **kwargs) #Place code that has to be executed after function call return result return wrapper
General template for writing a decorator factory
from functools import wraps def decorator_name(parameter1, parameter2, …): def actual_decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): #Place code that has to be executed before function call result = fn(*args, **kwargs) #Place code that has to be executed after function call return result return wrapper return actual_decorator
Applying decorators to imported functions
We can even apply our own decorators to the functions that we import from standard library or third-party packages. However, we cannot use the @ syntax for these functions.
from math import factorial from random import randint def trace(fn): def wrapper(*args, **kwargs): print(f'{fn.__name__} called') print(f'args : {args} kwargs : {kwargs}' ) result = fn(*args, **kwargs) print(f'Return value : {result}\n') return result return wrapper factorial = trace(factorial) randint = trace(randint) factorial(3) randint(5, 50)