Python: Decorators
A decorator is basically a function that takes another function as an argument.
It adds to or changes the functionality of that function
Can you live without dicorators?
Sure, you can add the needed extra functionality to your functions directly without using this decorators stuff, but you'd want to use decorators because they improves the following in your code:
- Readability
- Modularity
- Maintainability
How so? let's have a look at an example
Check out these simple functions
def multiply(num1, num2):
return num1 * num2
def sum(num1, num2):
return num1 + num2
Let's say we want to log the result of the opration before returning it:
def multiply(num1, num2):
result = num1 * num2
print(result)
return result
def sum(num1, num2):
result = num1 + num2
print(result)
return result
And here is how you can do the same using decorators:
def print_result(func):
def wrapper(num1, num2):
result = func(num1, num2)
print(result)
return result
@print_result
def multiply(num1, num2):
return num1 * num2
@print_result
def sum(num1, num2):
return num1 + num2
total = multiply(3, 4)
In the above example we've defined a decorator called print_result
. Now if we call multiply
or sum
we will see the result printed on the console:
12
Now imagine that instead of simply printing the result on the console we want to store the value in a file, or say we want to call an API and pass the result to it.
You may say I'm still not convinced, I can simply create a function and put all kinds of logic and API calls in it, then I'd place it in a different .py package for modularity, and when I need it I'd import it and use it inside the functions, something like this:
import logging_func as log
def multiply(num1, num2):
result = num1 * num2
log(result) # this would be the big messy function
return result
# and the same for sum()
The problem with this is that your functions and the log
are coupled. Which means that if you want to change the logging function (say from log
to log_it
you've have to go over every function that is using it and change it and make sure that you are not breaking things while doing so.
Also, note how clean and more readable the code is with decorators, I believe that this is very important.
Going back to our decorator:
def print_result(func):
def wrapper(num1, num2):
result = func(num1, num2)
print(result)
return result
Note that both multiply
and sum
take the same number of arguments, which we've named num1
and num2
But that's not practical, what if the functions take more than two parameters:
def multiply(num1, num2, num3, num4, ...etc
In this case our decorator can be modified to be more flexable:
def print_result(func):
def wrapper(*args): # Note the *args
result = func(*args) # Note the *args
print(result)
return result
return wrapper
In python putting *args
allows the function to take many arguments/parameters without the need to define them before hand.
Note: it's conventional to name it
args
but I encourge you to name it whatever makes sense, so in our example I can rename it*numbers
if I'll be using it only with functions that take only numbers as arguments.
So here is how our code looks like so far:
def print_result(func):
def wrapper(*args): # Note the *args
result = func(*args) # Note the *args
print(result)
return result
return wrapper
@print_result
def multiply(num1, num2):
return num1 * num2
total = multiply(4, 5)
It's also useful and common to make the decorator more flexable by allowing it to accept keyword arguments. This is how it's done:
def print_result(func):
def wrapper(*args, **kwargs): # Note the *args and **kwargs
result = func(*args, **kwargs) # Note the *args and **kwargs
print(result)
return result
return wrapper
@print_result
def multiply(num1, num2):
return num1 * num2
An example of using keyword arguments:
instead of total = multiply(4, 5)
we can pass the arguments like this total = multiply(num1=4, num2=5)
it will return the exact same result of course.
More info about keyword arguments can be found here
This was python decorators in a nutshell :)