What are Python Decorators ?
Python Decorators function is just a function that takes another function as an argument and add extra features or behaviour to another function without altering the source code of original function.
The python decorators take advantage of the fact that the functions in python are first class functions it means that we can pass a function as an argument to other function. We can return functions and assign functions to the variables.
What is the use of Python Decorators?
If we want to change the functionality or add a feature to an existing function to which we do not have access or we do not want to change, then we use decorators. Also, if there is certain kind of functionality like timing a function or logging a function, and we want to add this functionality to multiple functions in that case we can create a decorator function and then apply that decorator function to all those functions.
How to create a simple Python Decorators Function?
As discussed above, Python decorators function is nothing but a function which takes another function as an argument. So we can create it using the following syntax :-
def decorator_function(func):
def wrapper_function():
print (f'Wrapper function ran before {func.__name__}.')
return func()
return wrapper_function
How to apply Python Decorators to a function?
To apply the python decorators to a function first we will create a simple function which will just have a print statement:-
def a_function():
print ('A function ran.')
Now, to decorate it we can do the following :-
decorated_function = decorator_function(a_function)
decorated_function()
# Output
Wrapper function ran before a_function.
A function ran.
If you have a look at the above output, you will see that the print statement from the wrapper function of the python decorator ran first. But this is a longer way to apply a decorator to a function. The general syntax to use is to add @decorator_function at the very top of the function.
@decorator_function
def a_function():
print ('A function ran.')
a_function()
# Output
Wrapper function ran before a_function.
A function ran.
How to create Python Decorators with arguments?
In the above example our function didn’t take any argument but if we have a following function which takes certain arguments :-
def b_function(name, age):
print(f'{name} is {age} years old.')
Now, if we try to decorate it with our python decorators and run it, we will encounter an error:-
@decorator_function
def b_function(name, age):
print(f'{name} is {age} years old.')
b_function('Saral', 30)
TypeError: wrapper_function() takes 0 positional arguments but 2 were given
So, in order to create Python Decorators with arguments, we will have to add positional arguments (*args) and keyword arguments (**kwargs) to the wrapper function as under:-
def decorator_function(func):
def wrapper_function(*args, **kwargs):
print (f'Wrapper function ran before {func.__name__}.')
return func(*args, **kwargs)
return wrapper_function
@decorator_function
def b_function(name, age):
print(f'{name} is {age} years old.')
b_function('Saral', 30)
Wrapper function ran before b_function.
Saral is 30 years old.
Real world example of a Python Decorator
To see the Python Decorators working in a real-world example, we will create a simple divide function, which will take two numbers as the argument and print the result:-
def simple_divide(a, b):
print(a / b)
simple_divide(4,2)
simple_divide(2,4)
# Output
2.0
0.5
So, the above function is working as it should be and giving us the results, but if for some reason, you want the dividend to be always greater than the divisor, no matter what is the order of the passed arguments, you can create a if statement to switch the arguments. So, we can do it using the following code:-
def simple_divide(a, b):
if b > a :
a,b = b,a
print(a / b)
simple_divide(2,4)
# Output
2.0
So now the question is, if we can easily do it from the code itself, why do we need the decorators. A few reasons for that could be a situation that you are working on a team project and you do not have access to this function or there are plenty of codes in this project, in which you want to add similar functionality. In such case, the decorators come handy. So, we will create our decorator as under:-
def my_decorator(func):
def wrapper_function(a, b):
if b > a:
a, b = b, a
return func(a, b)
return wrapper_function
@my_decorator
def simple_divide(a, b):
print(a / b)
simple_divide(2,4)
# Output
2.0
Creating a Timing Decorator in Python Decorators
One of the frequently used Python Decorators is the timer decorator. If in your project, you want to check out how much time each function is taking then instead of adding code to all the functions separately you can create a Python Decorator and then decorate all the desired function using that decorator. The code for creating the timing decorator in Python is as under:-
def timer(func):
import time
def wrapper_function(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} took {end-start} sec(s) to run.')
return wrapper_function
import time
@timer
def a_function():
time.sleep(1)
print('A function ran.')
a_function()
# Output
A function ran.
a_function took 1.0029809474945068 sec(s) to run.
Creating a logging/logger decorator in Python.
Say, you have recently learnt the logging and now you are excited and apply it to all your fucntions in a project. You can easily do that using Python Decorators. The code for creating a Logging Decorator in Python is as under:-
def my_logger(func):
import logging
logging.basicConfig(filename=f'{func.__name__}.log', level=logging.INFO)
def wrapper_function(*args, **kwargs):
logging.info(f'Ran with args: {args} and kwargs: {kwargs}')
return func(*args, **kwargs)
return wrapper_function
@my_logger
def b_function(name, age):
print (f'{name} is {age} years old.')
b_function()
Running this will create a new log file b_function.log. Check out our complete tutorial on Python Logging.
Adding more than one decorators in Python
You can also add more than one decorators in Python as under:-
@my_logger
@timer
def a_function():
time.sleep(1)
print('A function ran.')
a_function()
But running this will print the statement and then create a logging file with the name wrapper_function.log. This is not what we want. This happens because the above statement is equivalent to
my_logger(timer(a_function))
And since timer(a_function) returns the wrapper_function, the resultant log file is created with it’s name.
To overcome this issue, we will have to import another wrapper function called wraps and then wrap our decorator functions using that.
from functools import wraps
def timer(func):
import time
@wraps(func)
def wrapper_function(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} took {end-start} sec(s) to run.')
return wrapper_function
def my_logger(func):
import logging
logging.basicConfig(filename=f'{func.__name__}.log', level=logging.INFO)
@wraps(func)
def wrapper_function(*args, **kwargs):
logging.info(f'Ran with args: {args} and kwargs: {kwargs}')
return func(*args, **kwargs)
return wrapper_function
How to create a Python Decorator Class?
Instead of creating a Python Decorator Function, you can also create a Python Decorator Class as under:-
class decorator_class(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(f'Call Method ran before {self.func.__name__}.')
return self.func(*args, **kwargs)
@decorator_class
def a_function():
print ('A function ran.')
a_function()
# Output
Call Method ran before a_function.
A function ran.
Video Tutorial on Python Decortors
Don’t forget to check out our detailed video tutorial on Python Decorators