How to Think About Python Decorators

One of the things I’ve struggled with while coding in python is decorators. I’ve always known that they could be useful, but didn’t have a solid intuition on their implementation. Every time I thought I had a grasp on decorators, I’d try to use them and fail.

Thus, I am writing this as a guide for myself, but also for the benefit of anyone who might benefit from my understanding. All code for this write up was run in python 3.6.

So what does a decorator do?

A decorator takes a function and modifies the behaviour around it. Less technically this means that it can do things before and after the function runs. They generally take the form:

def decorator(function_to_decorate):
    def function_wrapper(*args, **kwargs):
        # do things
        return function_to_decorate(*args, **kwargs)
    return function_wrapper

And are used in code like this:

@decorator
def decorated():
    #do things

Which is equivalent to:

def decorated():
    #do stuff
decorated = decorator(decorated)

You would then use the decorated function normally.

Now in this simple example the decorated function has no arguments thus the use of args and kwargs in function_wrapper. But if you had arguments, you could manipulate them in the decorator before you pass them to the decorated function:

def decorator(function_to_decorate):
    def function_wrapper(*args, **kwargs):
        print(f'We have access to {args} in function_wrapper')
        return function_to_decorate(*args, **kwargs)
    return function_wrapper

If you decorated a function using this decorator and called it in you code like so:

@decorator
def decorated(argument):
    return f'{argument} was passed to decorated'

print(decorated('Sidney'))

The output would be (note what was printed first):

We have access to ('Sidney',) in function_wrapper
Sidney was passed to decorated

Note the code from the last section is equivalent to:

def decorated(argument):
    return f'{argument} was passed to decorated'

print(decorator(decorated)('Sidney'))

This confused me for the longest time but I came to understand it when I looked at it in layers. As I’ve said before the when we use the decorator on the decorated function it’s equivalent to:

decorator(decorated)

But what this means is we’re now in the function_wrapper.

decorator(decorated).function_wrapper

So when we call our decorated function with an argument we’re passing that argument to function_wrapper

decorator(decorated).function_wrapper(arguments)

And then we do things: in our example we print which argument we have access to. So thinking about it in levels:

  1. When we decorate a function: the decorator takes the decorated function and exposes function_wrapper
  2. When we called the decorated function in our code later: it starts to run the code in function_wrapper (with the arguments if they’re there)
  3. In function_wrapper: after the code is run we can called the function that was decorated from decorator. We pass any arguments to it.

Decorators are powerful instruments allowing us to keep our code clean but also DRY(don’t repeat yourself) principle complaint. I plan to use it a lot more in my code particularly as I build the Reja Analytics system at Intelipro. I’m no means an expert at this but if you have an questions, reach out on Twitter. Peace!!!

Initial inspiration for this post came from this post. A rather thorough look at decorators can be found from this post.

Leave a Reply

Your email address will not be published. Required fields are marked *

two × 1 =