Decorators are functions which modifies the functionality of other functions (instead of adding more codes to original function, decorators can be used). Decorators wrap around the main function and allow us to operate on inputs and outputs. Firstly let us understand functions in python -

def operations(x):
    """Incrementing x by 1"""
    x += 1
    return x

operations(x=1)

Now let’s say we want to increment our input x by 1 before and after the operations function executes and, to illustrate this example, here’s how we would do it by changing the original code:

def operations(x):
    """Basic operations."""
    x += 1
    x += 1
    x += 1
    return x

operations(x=1)

Output

4

We were able to achieve what we want but we now increased the size of our operations function and if we want to do the same incrementing for any other function, we have to add the same code to all of those as well which is not very efficient. This is where decorators comes in to use. Lets understand how to create a decorator -

The decorator function accepts a function f which is the function we wish to wrap around, ( in our case, it’s operations()). The output of the decorator is its wrapper function which receives the arguments and keyword arguments passed to function f.

Inside the wrapper function, we can :

  1. extract the input parameters passed to function f.
  2. make any changes we want to the function inputs.
  3. function f is executed
  4. make any changes to the function outputs
  5. wrapper function returns some value(s), which is what the decorator returns as well since it returns wrapper.

Decorator

def add(f):
    def wrapper(*args, **kwargs):
        """Wrapper function for @add."""
        x = kwargs.pop("x")               # .get() if not altering x
        x += 1                            # executes before function f
        x = f(*args, **kwargs, x=x)
        x += 1                            # executes after function f
        return x
    return wrapper

We can use this decorator by simply adding it to the top of our main function preceded by the @ symbol.

@add
def operations(x):
    """Basic operations."""
    x += 1
    return x

operations(x=1)

output

4

The function name and docstring are not what we’re looking for but it appears this way because the wrapper function is what was executed. In order to fix this, Python offers functools.wraps which carries the main function’s metadata.

from functools import wraps

# Decorator
def add(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        """Wrapper function for @add."""
        x = kwargs.pop("x")
        x += 1
        x = f(*args, **kwargs, x=x)
        x += 1
        return x
    return wrap

@add
def operations(x):
    """Basic operations."""
    x += 1
    return x

Using decorators, we managed to decorate our main function operation() to achieve the customization we wanted without actually altering the function. We can reuse our decorator for other functions that may need the same customization!