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 :)