Python decorators in a nutshell.
Tags: python, decorators
Published: 2019-02-06 02:35:58

---
title: Python decorators in a nutshell.
published: true
description: Let's talk about decorators in Python, one of the features that made me fall in love with the language.
tags: python, decorators
cover_image: https://thepracticaldev.s3.amazonaws.com/i/brvii68uxgwerxp4ykiv.png
---

## What's a decorator anyway?

Decorators are a way to extend the behavior of pieces of software without actually having to modify them.

In Python, in particular, they are functions that can change the behavior of other functions without changing their code. They provide a transparent way of adding satelital functionalities to already defined functions.

For example, imagine we want to measure the time it takes to execute a couple functions in our program. We could code a timer around their behavior by modifying them all, **or** we can create a decorator, centralizing the logic of timing a function within one place and applying it to several functions.

## A couple concepts we need to know first...

### In Python, functions are first-class objects.

This means that functions can be assigned to variables and parameters, and even be returned by other functions.

Functions that return other functions, or that receive other functions as arguments, are called higher order functions.

```
def prepare_hello(name: str):
    def say_hello():
        print("Hello, {}!".format(name))
    return say_hello

hello_brian = prepare_hello("Brian")
hello_brian()
```

On the above snippet, `prepare_hello` is a higher order function, as it returns another function. Also, we can see how we can assign functions to variables, as `hello_brian` is a variable that holds the function returned by `prepare_hello`.

A couple known higher order functions in Python are `map`, `filter`, `sort` and `reduce`. These are functions that receive a collection *and a function*  as argument, to let us process the collection using the given function in different ways.

### `*args` and `**kwargs`.

You may already know these guys, they are pretty famous around Python. Both `*args` and `**kwargs` allow you to pass a variable number of arguments to a function. Your function is basically a blank check if the signature contains one or both of these guys.

```
def blank_check(*args, **kwargs):
    for arg in args:
        print("Arg: {}".format(arg))
    for keyword, arg in kwargs.items():
        print("Kw: {}, Arg: {}".format(keyword, arg))

blank_check(1, 2, 3, hello="Hello", word="World")
```

Here's the output of the above function:

```
Arg: 1
Arg: 2
Arg: 3
Kw: hello, Arg: Hello
Kw: word, Arg: World
```

As you can see, arguments passed without a keyword are handled by `*args`, and arguments passed with a keyword are handled by `**kwargs`.

This allows us to write functions that do not care at all about the arguments they receive, which will make a hell of a lot easier to write versatile decorators.

A thing to notice is that the important part of `*args` and `**kwargs` are the aesterisks. The name can be anything, as any other variable, `args` and `kwargs` are used by convention, as this practice is dark-ish and needs to be spotted right away.

## Now, decorators!

Decorators are, basically, functions that wrap other functions. In Python it really is as easy as that.

```
def a_decorator(wrapped_function):
    def wrapper(*args, **kwargs):
        print('I am the decorator!')
        return wrapped_function(*args, **kwargs)
    return wrapper
```

That's the definition of a decorator, right there. You can see two interesting things there:

* The decorator receives the function it is supposed to wrap as an argument, **and** returns a wrapper function. Therefore, a decorator is a higher order function.
* The decorator, by using `*args` and `**kwargs`, does not care **at all** what are the arguments of the wrapped function, it just does its thing and calls the wrapped function with all the given arguments.

Now, we can use it wherever we want:

```
@a_decorator
def sum_numbers(num_a, num_b):
    return num_a + num_b

print(sum_numbers(1, 2))
```

The output will be:

```
I am the decorator!
3
```

It is really transparent, see? And although it is pretty magical, it is really explicit. You go to the definition of the function and you see right there, in your face, what decorators are being executed.

This implementation, though, has a subtle issue:

```
print(sum_numbers.__name__)
```

That will print out `wrapper`, as that's the name of the `wrapper` function the decorator returns. Now, when debugging, that thing right there will be a pain in the butt. But we can fix it! And we can fix it with the standard library! :D

There is a module in python called `functools` that provides a lot of awesome things to work with functions. I really encourage you to go and see what it has to offer, but today we are just going to use one decorator it provides.

The decorator `functools.wraps` receives a function as argument and basically overrides the wrapped function properties with the ones of the given function.

Let's change our decorator using `functools.wraps` to see this in action.

```
from functools import wraps

def a_decorator(wrapped_function):
    @wraps(wrapped_function)
    def wrapper(*args, **kwargs):
        print('I am the decorator!')
        return wrapped_function(*args, **kwargs)
    return wrapper

@a_decorator
def sum_numbers(num_a, num_b):
    return num_a + num_b

print(sum_numbers.__name__)
```

That will print `sum_numbers` again, fixing our issue elegantly. Now, how come `wraps` receives parameters? How do I do that? You'll need an intermediate function that receives those parameters and returns the wrapper function.

Your decorator will not actually be a decorator, it will be a decorator *builder*, which receives a couple of arguments and returns a decorator.

```
def a_decorator(name):
    def actual_decorator(wrapped_function):
        @wraps(wrapped_function)
        def wrapper(*args, **kwargs):
            print('I am the decorator, {}!'.format(name))
            return wrapped_function(*args, **kwargs)
        return wrapper
    return actual_decorator

@a_decorator("Sum Numbers")
def sum_numbers(num_a, num_b):
    return num_a + num_b

print(sum_numbers(1, 2))
```

## The actual problem we wanted to solve

Let's say, we have a function we want to time:

```
def fibonacci(n, a = 0, b = 1):
    if n == 0: 
        return a 
    if n == 1: 
        return b 
    return fibonacci(n - 1, b, a + b)
```

Now, let's just drop the idea of adding timing functionality directly to it, as it would pollute its code, making it more complex to follow, just because we wanted to add non-critical behavior to it.

So, let's write a decorator called `timer` that receives a cool operation name (to make it more readable in the logs) and add `@timer("Fibonacci calculator")` to the original function.

```
from datetime import datetime
from functools import wraps

def timer(operation_name):
        def timer_decorator(wrapped_function):
                @wraps(wrapped_function)
                def wrapper(*args, **kwargs):
                        start = datetime.now()
                        result = wrapped_function(*args, **kwargs)
                        end = datetime.now()
                        print("{} took {}.".format(operation_name, end - start))
                        return result
                return wrapper
        return timer_decorator

@timer("Fibonacci calculator")
def fibonacci(n, a = 0, b = 1):
    if n == 0:
        return a
    if n == 1:
        return b
    return fibonacci(n - 1, b, a + b)

fibonacci(30)
```

Now, in the output we'll see how much time each execution of fibonacci took, taking into account it is a recursive function and for `n=30` it will execute itself 30 times. So we'll see in the output `Fibonacci calculator took <time>` thirty times.

This experiment also makes it very clear that Python doesn't do tail recursion optimization, so keep that one in mind :P.

## Conclusion

Python makes decorators fairly easy, and they are really awesome for this kind of features we all need in production environments. 

Caching, unit testing, routing, these are all problems that can be elegantly solved using decorators, and I think, particulary in Python, they are really beginner friendly.