Download Kite Free! Install Kite Free!

Python Decorators

Andy Miles
March 18, 2019

Table of Contents

Decorators are quite a useful Python feature. However, it can seem that any resources or insights surrounding them makes the whole concept impossible to understand. But decorators are, in fact, quite simple. Read on, and we’ll show you why.

Why do I need a decorator?

Let’s start by ignoring any Python or software for a moment, and instead illustrate the concept using a real life scenario.

Very early in life we learn to move by walking. Later in life, we may learn to move by riding a bicycle. And driving a car. And perhaps riding a skateboard. But whichever way we learn, we are still just moving, the same as we always have.

To understand the decorator concept, imagine that walking, riding a bicycle, driving a car, and riding a skateboard are all behaviors that augment moving: they are decorating the move behavior.

In short, this is the exact concept of the decorator!

“Cycling” is a behavior that “decorates” the way something, in this case, a person, moves. Walking, driving, and riding a bicycle all represent alternative ways of moving that can be applied not only to the behaviors of a person, but also to other applicable objects. (A dog, for instance, could walk, and possibly ride a skateboard. I’m not sure he could get a driver’s license though!)

So, now that we have described the concepts, let’s take a look at some Python:

>>> def calculate_amount(premium, rate):
... return rate * premium
...
>>>

This is a simple function that calculates an amount after applying interest. And we use it in various applications to calculate the impact of interest. For example, this way:

>>> total = calculate_amount(120, 1.10)
>>> total
132.0
>>>

Now, we’re going to implement a self-service web application that allows our customers to submit loan requests. This web application will use the same interest function. However, since it’s going to be used by customers rather than our own customer service reps, we need to log the results of the calculation to have an audit trail of the customers’ own calculations.

Note that the calculation and the business logic are identical in either case. However, we want to use a technical feature – logging – to address a supplemental business requirement. A good design allows us to decouple differing concepts, especially those that relate to our business logic vs. the technology we use. It also accounts for concepts that change at different time.

Consider that a change in technology, such as an upgraded third-party component, might have us upgrade our logging logic. We want to avoid having to have to touch the business logic: it increases the likelihood of us breaking something, which may result in additional testing. These extra steps would increase implementation time and risk.

This is where decorators get to shine! A decorator embellishes our business logic without changing it, and mitigates the risks discussed above. Not only that, but it allows us to selectively use it to only log things we really need to log – and do so easily. This way, unnecessary logging that could slow down performance is eliminated.

This is why we are going to use a decorator, rather than develop, say, a log_calculate_amount function.

Next, let’s walk through the thought process for designing a solution that enables logging.

Intro to logging

What we have described above turns out to be a pattern that indicates a possible need for a decorator. Decorators allow us to add behavior to an existing function without changing the original behavior in any way. That is – to use the example we started with – we can still move, but we can also ride a bicycle or a skateboard.

Let’s see how a decorator works and start with an aside to introduce the logging concept.

For the sake of this specific example, you’re potentially already using logging in your Python code. If you don’t, or if you use the standard logging module, let me introduce you to a fantastic and easy to use new logging module called Loguru.

Loguru is simple to configure and use, and requires minimal setup code to start logging. The Python standard logging module is powerful and flexible, but can be difficult for beginners to configure. Loguru gives us the best of both worlds: you can start simple, and even have the bandwidth to drop back to standard Python logging for more complex logging scenarios. You can take a look at the link mentioned above to learn more.

Since we are using our decorator to introduce logging, let’s look at how we get logging to work.

Setting up logging in Loguru

First:

pip install loguru

Then, start a new Python module. The first statement will be:

from loguru import logger

Now we can get back to decorators.

Recall we have the calculate_amount function for which we want to log execution when it is used in certain cases:

def calculate_amount(premium, interest):
return premium * interest

With a decorator, which we will look at in a minute, all you need to do is add the name of the decorator prior to defining the function, like this:

@log_me
def calculate_amount(premium, interest):
return premium * interest

So in this case the decorator is called @log_me

Without the decorator, we see that the function returns a number like 132, representing the amount with interest. We still get that with the decorator, but more besides. We’ll see more of this type of behavior as we peek at the functionality the decorator might offer behind the scenes.

Implementing the decorators

I start by defining a function to implement my decorator that looks like this:

def log_me(func):

Notice that the function name is identical to what appears after the @ in the decorator itself. Also notice that I named the parameter func. That is because log_me takes a function as its input.

Now, let’s implement the decorator in its entirety.

Note that while looking over the code, the function (inner) is defined within another function (log_me). In this case, we can see that Python allows defining functions inside other functions, and sometimes depends on it. We say that inner is a wrapper for func. This means that when we decorate any function (func in the code below) with @log_me, then that function is wrapped with additional logic (as shown in inner below).

We’ll explain it line by line:

def log_me(func):
def inner(a,b):
logger.info(f"{__name__} calculated with {a}, {b}")
return func(a,b)
return inner

The first statement inside log_me defines another function, called inner, that wraps the function we’re decorating (in this case we are decorating calculate_amount).

We define inner as taking two parameters, a and binner then executes a logger statement from loguru that logs the details of what we’re being asked to calculate.

inner then returns the value of the function that was passed to log_me with its parameters, which in turn is returned by log_me itself.

Now when we have this definition:

@log_me
def calculate_amount(premium, interest):
return premium * interest

…and run this code:

amount = calculate_amount(120, 1.10)

We see:

2019-02-24 09:51:38.442 | INFO     | __main__:inner:8 - __main__ calculated with 120, 1.1

The decorator, using loguru in this case, adds details for us about when the calculation is requested, and what values were requested for premium and interest.

Now, we can add the decorator wherever it’s needed, and get to logging for free!

Final notes and going forward

We’ve now seen how decorators help us separate business and technical concepts, and apply logic only where it’s needed. Decorators may seem more intuitive to use than define, but defining very complex decorators can too become second-nature with practice.

You’ll notice that decorators are used extensively in modern Python code. Some applications I’ve used personally include memorizing values (that is, improving performance by having functions “remember” values they have calculated in prior invocations,) and in Pytest harnesses, for storing test data my own tests have used. Additionally, you might encounter entire packages built on the concept of decorators – especially web frameworks like Flask. In these cases, decorators allow you to focus on the behavior of a route or endpoint without worrying how the framework implements the callback logic.

Can you work out how to log the results of the calculation too, with a decorator? Another exercise might be discovering how to add decorated methods to a class specification. The bottom line: consider using a decorator for anything that want to transparently “wrap” with additional functionality.