Decorator is a basic structure design that allows programmers to expand and alter the behavior of a function, a method, or a class without changing their code.
The principle thought is that we place those callable objects, the functionality of which we have to change, inside a different object with new behavior. In this way, decorators are only wrappers around the initial objects.
Mostly, we use them to pass a function as an argument to a decorator to call this function later and perform certain activities before and after the call.
In Python, the standard syntax for decorators is the @ sign preceding the name of a decorator, and afterward the object we need to decorate on the next line with the same indentation.
Decorators are called immediately before the body of a function, the behavior of which we might want to change. Here is an example of how the overall structure should resemble:
@decorator_function def func(): .....
Now, to better understand how it works, let’s see how to write our own simple decorator.
def our_decorator(other_func): def wrapper(args_for_function): print('This happens before we call the function') return other_func(args_for_function) return wrapper
Here we define the function our_decorator: it accepts another function as its argument and contains a wrapper that prints a message and calls the function that we’ve passed to our_decorator. Then we return this wrapper function that contains our changed one.
Presently, we define a function greet utilizing our_decorator:
@our_decorator def greet(name): print('Hello,', name)
On calling greet the output will be:
greet('Susan') #output #This happens before we call the function #Hello, Susan
However, not always do you need to write your decorators, sometimes you can just use decorators from the Python standard library.
Chaining decorators in Python
Multiple decorators can be nested in Python.
This is to state, a function can be decorated multiple times with various (or same) decorators. We basically place the decorators over the desired function.
def mod(func): def inner(*args, **kwargs): print("%" * 30) func(*args, **kwargs) print("%" * 30) return inner def dash(func): def inner(*args, **kwargs): print("-" * 30) func(*args, **kwargs) print("-" * 30) return inner @mod @dash def printer(msg): print(msg) printer("Welcome") #output #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% #------------------------------ #Welcome #------------------------------ #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
The above syntax of,
@mod @dash def printer(msg): print(msg)
is equivalent to
def printer(msg): print(msg) printer = mod(dash(printer))
The order in which we chain decorators matter. If we had reversed the order as
@dash @mod def printer(msg): print(msg)
The output will be
#output #------------------------------ #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% #Welcome #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% #------------------------------
Why use decorators?
The most significant purpose behind utilizing decorators is that they are a method of making your code increasingly more readable and clean.
Imagine that we have a set of functions. We need to measure, for example, how long it takes for every one of them to perform the algorithms so we include a timer in each code block:
import time def func1(args_for_function): start = time.time() # gets the current time ... # something happens here end = time.time() print('func1 takes', end - start, 'seconds') def func2(args_for_function): start = time.time() ... end = time.time() print('func2 takes', end - start, 'seconds')
Although, when it is done, the two after issues may arise:
- Specific lines would show up and be repeated in each function: the ones with start and end in our case
- These lines would be repetitive to the actual functionality and the initial code.
These issues can be solved with a different reusable example that may then be applied to some other function. For our situation, we can make it like this:
def timer(func): def wrapper(args_for_function): start = time.time() func(args_for_function) end = time.time() print('func takes', end - start, 'seconds') return wrapper @timer def func1(args_for_function): ... # define your function
In the example above, we have written a function decorator timer() that takes any function as an argument, notes the time, invokes the function, notes the time again, and prints how much time it took. Thus, we can use this decorator for any function later on and there will be no need to modify the code of the functions itself.
Python decorator handling exception
The following code will print the value of an array with respect to its index.
array = ['hey', 'hi', 'hello'] def valueOf(index): print(array[index]) valueOf(0) #output #hey
But what if we call the function for,
valueOf(10) #output #IndexError: list index out of range
As we haven’t handled whether the given index value exceeds the size of the array or not. So now we will decorate this function as follows:
array = ['hey', 'hi', 'hello'] def decorator(func): def newValueOf(pos): if pos >= len(array): print("Sorry! Array index is out of range") return func(pos) return newValueOf @decorator def valueOf(index): print(array[index]) valueOf(10) #output #Sorry! Array index is out of range
Now, we should go over the primary points we have learned in this topic:
- Decorators permit us to change the behavior of the object without changing its source code
- They are introduced with the @ symbol directly before the function, the function of which we need to modify
- To make custom decorators, we have to specify a decorator function that will return a wrapper over the given function.