5 Advanced Tips on Python Decorators

Do you want to write concise, readable, and efficient code? Well, python decorators may help you on your journey.

Michael Berk
Towards Data Science

--

Photo by Mauricio Muñoz on Unsplash

In chapter 7 of Fluent Python, Luciano Ramalho discusses decorators and closures. They are not super common in basic DS work, however as you start building production models writing async code, they become an invaluable tool.

Without further ado, let’s dive in.

1 — What’s a decorator?

Before we get into the tips, let’s cover how decorators work.

Decorators are simply functions that take a function as input. Syntactically they’re often depicted as @my_decoratorin the line above a “decorated” function, for example…

temp = 0def decorator_1(func):
print('Running our function')
return func
@decorator_1
def temperature():
return temp
print(temperature())

However, what’s really going on under the hood is when we call temperature(), we’re just running decorator_1(temperature()), as shown below…

temp = 0def decorator_1(func):
print('Running our function')
return func
def temperature():
return temp
decorator_1(temperature())

Ok, so decorators are functions that take another function as an argument. But why would we ever want to do this?

Well, decorators are really versatile and powerful. They’re commonly used for asynchronous callbacks and functional-style programming. They can also be leveraged to build class-like functionality into functions, thereby reducing development time and memory consumption.

Let’s get into some tips…

2 — The Property Decorator

Tip: use the built-in @property to augment setter/getter functionality.

One of the most common built-in decorators is @property. Many OOP languages, such as Java and C++, suggest using a getter/setter paradigm. These functions are used to ensure that our variable will not return/be assigned incorrect values. One example could be requiring our temp variable to be greater than absolute zero…

We can augment a lot of this functionality using the @property method, making our code more readable and dynamic…

Note that we removed all of the conditional logic from my_setter() for brevity, but the concepts are the same. Isn’t c.temperature a lot more readable than c.my_getter()? I certainly think so.

But before we move on there’s one important caviat. In python, there is no such thing as a private variable. The _ prefix indicates that the variable is protected and should not be referenced outside the class. However, you still can…

c = my_vars(500)
print(c._temp) # 500
c._temp = -10000
print(c._temp) # -1000

The fact that truly private variables don’t exist in python was an interesting design choice. The argument is that private variables in OOP aren’t actually private — if someone wanted to access them they could change the source class’s code and make the variables public.

Python encourages “responsible development” and allows you to externally access anything in a class.

3 — Classmethod and Staticmethod

Tip: use the built-in @classmethod and @staticmethod to augment class functionality.

These two decorators are commonly confused, but their difference is very straight forward.

  • @classmethod takes the class as a parameter. It’s bound to the class itself, not the class instance. Therefore, it can access or modify the class across all instances.
  • @staticmethod does not take the class as a parameter. It’s bound to the class instance, not the class itself. Therefore, it cannot access or modify the class at all.

Let’s take a look at an example…

The biggest use case for classmethods is their ability to serve as alternative constructors for our class, which is really useful for polymorphism. Even if you’re not doing crazy stuff with inheritance, it’s still nice to be able to instantiate different versions of the class without if/else statements.

Static methods, on the other hand, are most often used as utility functions that are completely independent of a class’s state. Notice that our isAdult(age) function doesn’t require the usual self argument, so it couldn’t reference the class even if it wanted to.

4 — A Quick Tip

Tip: use @functools.wraps to preserve function information.

Remember, decorators are just functions that take another function as an argument. So, when we call decorated functions, we’re actually calling the decorator first. This flow overwrites information about the decorated function, such as the __name__ and __doc__ fields.

To overcome this problem, we can leverage another decorator…

Without the @wraps decorator, the output of our print statements is the following.

print(f(5))        # 30
print(f.__name__) # 'call_func'
print(f.__doc__) # ''

To avoid overwriting important function information, be sure to use the @functools.wraps decorator.

5 — Create Custom Decorators

Tip: build your own decorators to augment your workflow, but be careful.

Variable scope in decorators is a bit weird. We don’t have the time get into the nitty gritty, but here’s a 29 minute article if you’re that dedicated. Just note that if you get the following error, read up on decorator scope:

python decorators inehertance class programming

With that disclaimer let’s go ahead and look at some useful custom decorators…

5.1 — Store Functions Based on Decorator

The below code appends functions to a list when they’re called.

One potential use case is for unit testing, just like with pytest. Let’s say that we have fast and slow tests. Instead of manually assigning each to a separate list, we can just add a @slow or @fast decorator to each function, then call each value in the corresponding list.

5.2 — Time Data Queries or Model Training

The below code prints the runtime of your function.

If you’re running any type of data query or training a model with bad logs, it’s really useful to have an estimate of run time. Just by having a @time_it decorator, you can get runtime estimates for any function.

5.3 — Perform Flow Control on Function Inputs

The below code performs conditional checks on function parameters prior to executing the function.

This decorator applies conditional logic on all of our functions’ parameter x. Without the decorator we’d have to write that if is not None flow control into each function.

And these are just a few examples. Decorators can be really useful!

Thanks for reading! I’ll be writing 18 more posts that bring academic research to the DS industry. Check out my comment for links to the main source for this post and some useful resources.

--

--