10 Fabulous Python Decorators | by Emmett Boudreau | Towards Data Science

A great thing about the Python programming language is all of the features that it packs into a small package that are incredibly useful. A lot of said features can completely alter the functionality of Python code, which makes the language more versatile. Furthermore, some of these functionalities can serve to shorten the amount of time it takes to write effective software when used properly. A great example of a Python feature that accomplishes these objectives very well is Python’s decorators.
Decorators are quick programming macros that can be used to alter the behavior of a Python object. They can be applied to classes and functions, and can actually do a lot of really interesting things! Decorators can serve to shorten code, speed up code, and completely change the way that code acts in Python. Needless to say, this can certainly come in handy! Today I wanted to show off some decorators that I think are worth checking out. There are a lot of decorators, but I picked some of the ones I think have the coolest functionality.
The first decorator on this list comes to us from the functools module. This module is included in the standard library, and is incredibly easy to use. It also contains a lot more cool features than just this decorator, but this decorator is certainly my favorite of the bunch. This decorator can be used to speed up consecutive runs of functions and operations using cache. Of course, this should be used with some notes on swap and caching in mind, but in a general-purpose case of use, most of the time this decorator is worth using. If you would like to learn more about Functools and why I love it, I actually wrote an entire article on just that you can read here:
towardsdatascience.com
Being able to speed up code with a simple decorator is incredibly awesome. A great example of a function that could benefit from a decorator like this is a recursive function, such as this function that calculates a factorial:
def factorial(n):    return n * factorial(n-1) if n else 1
Recursion can be pretty hard on calculation times, but adding this decorator can help to speed up consecutive runs of this function dramatically.
@lru_cachedef factorial(n):    return n * factorial(n-1) if n else 1
Now whenever we run this function, the first few factorial calculations will be saved into cache. As a result, next time we go to call the function we will only need to calculate the factorials that are after the ones we used prior. Of course, not all factorial calculations will be saved, but it is easy to see why this is a great application of this decorator to speed some code up that is naturally slow.
JIT is short for Just In Time compilation. Normally whenever we run some code in Python, the first thing that happens is compilation. This compilation creates a bit of overhead, as types are allocated memory and stored as unassigned but named aliases. With Just In Time compilation, we do most of this work at execution. In a lot of ways, we can think of this as something akin to parallel computing, where the Python interpreter is working on two things at once in order to save some time.
The Numba JIT compiler is famous for providing that very concept into Python. Similarly to the @lru_cache, this decorator can be called pretty easily with an immediate boost to performance in your code. The Numba package provides the jit decorator, which makes running more intensive software a lot easier without having to drop into C. If you would like to read more on this package, I also have another article all about it you can check out here:
towardsdatascience.com
from numba import jitimport random@jit(nopython=True)def monte_carlo_pi(nsamples):    acc = 0    for i in range(nsamples):        x = random.random()        y = random.random()        if (x ** 2 + y ** 2) < 1.0:            acc += 1    return 4.0 * acc / nsamples
The do_twice decorator does pretty much what is in its name. This decorator can be used to run a function twice with a single call. There are certainly some uses for this, and I have found it especially helpful with debugging. Another great thing it can be used for is measuring performance for two different iterations. Using Functools as an example, we could make a function run twice in order to check for improvements that come about. This function is provided by the decorators module in Python, which is in the standard library.
from decorators import do_twice@do_twicedef timerfunc():    %timeit factorial(15)
Going along with the simplicity of the do_twice decorator is the count_calls decorator. This decorator can be used to provide information as to how many times a function is used in software. Like do_twice, this certainly can come in handy for debugging. When added to a given function, we will receive an output telling us how many times the function has been ran each time it runs. This decorator is also in the decorators module from the standard library.
from decorators import count_calls@count_callsdef function_example():    print("Hello World!"function_example()function_example()function_example()
One of the best decorators I utilize all the time in order to save time when writing classes is the dataclass decorator. This decorator can be used to quickly write common standard methods that are typically found in the classes that we write. If you would like to learn more about this decorator, I also have an article on it that you can read here:
towardsdatascience.com
This decorator comes from the dataclass module. This module is also in the standard library, so there is no need for PIP to try out this example!
from dataclasses import dataclass@dataclassclass Food:    name: str    unit_price: float    stock: int = 0            def stock_value(self) -> float:        return(self.stock * self.unit_price)
This code will automatically create an initialization function, __init__(), with the positional arguments necessary to fill the data in our class. They will also be provided to self automatically, so there is no need to write a long function just to get some data arguments into a class.
In order to understand the purpose of the singleton decorator, we need to first understand what a singleton is. Singletons are in a sense a version of global types. This means that the types are defined to only exist once. Although these are quite common in languages like C++, it is pretty rare to see them in Python. With singletons, we create a class to only be used once and mutate the class rather than an initialized constructed type. In this case, types act less as templates and more as a single controlled object.
Typically, singleton decorators are made by the user, and not in fact imported. This is because the singleton is still a reference to the template provided in our singleton decorator. We can name a singleton function and write a wrapper in order to use this decorator on our class:
def singleton(cls):    instances = {}    def wrapper(*args, **kwargs):        if cls not in instances:          instances[cls] = cls(*args, **kwargs)        return instances[cls]    return wrapper@singletonclass cls:    def func(self):
Another way you could approach this problem is by using metaclasses. If you would like to learn more about metaclasses, I wrote an article last year that goes into more details on these and what they are used for you can view here:
towardsdatascience.com
class Singleton(type):    _instances = {}    def __call__(cls, *args, **kwargs):        if cls not in cls._instances:            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)        return cls._instances[cls]        class Logger(object):    __metaclass__ = Singleton
One decorator that might come in handy quite often for scientific computing is the use_unit decorator. This decorator can be used to alter the output of returns to methods. This can be useful for those who don’t want to add units of measurement to their data, but still want people to know what those units are. This decorator is also not really available in any module, but is very commonly written and incredibly useful for scientific applications.
def use_unit(unit):    """Have a function return a Quantity with given unit"""    use_unit.ureg = pint.UnitRegistry()    def decorator_use_unit(func):        @functools.wraps(func)        def wrapper_use_unit(*args, **kwargs):            value = func(*args, **kwargs)            return value * use_unit.ureg(unit)        return wrapper_use_unit    return decorator_use_unit@use_unit("meters per second")def average_speed(distance, duration):    return distance / duration
Function wrappers are a design pattern used when dealing with relatively complicated functions in Python. Wrapper functions are typically used to do what might be considered some more low-level, iterative tasks. The benefit of a wrapper function is that it can be used to dramatically improve performance. Like the lru_cache decorator, this decorator is provided by the FuncTools package.
import functools as ft
The wraps decorator itself is simply a convenience decorator for updating the wrapper of a given function. When this decorator is called, the function will update its wrapper each time it is used. This can go a long way in terms of improving performance, as is the case with many tools available in FuncTools.
def my_decorator(f):    @wraps(f)    def wrapper(*args, **kwds):        print('Calling decorated function')        return f(*args, **kwds)    return wrapper
Wrappers themselves are typically decorators, and the wraps decorator call is typically used on a wrapper function within that decorator call. After writing that code, we can decorate our new function with that wrapper:
@my_decoratordef func(x):    print(x)
In some cases, there might be circumstances where one might want to be able to access things that are defined privately as in a more global sense. Sometimes we have functions that are contained within classes that we want to have methodized, and this is exactly what the staticmethod decorator is used for.
Using this decorator, we can make C++ static methods for working with our classes. Normally, methods that are written in the scope of a class will be private to that class, and not accessible unless called as a child. There are circumstances, however, where you might want to take a more functional approach with the way your methods interact with data. Using this decorator, we can create both options without the need for creating more than one function.
class Example:    @staticmethod    def our_func(stuff):        print(stuff)
We also do not need to explicitly provide our class as an argument. The staticmethod decorator takes care of this for us.
FuncTools strikes again in this list with the wonderfully useful singledispatch decorator. Single dispatch is a programming technique that is quite commonplace in many programming languages because of just how great of a way it is to program. While I tend to prefer multiple dispatch, I think that single dispatch can be used to fill the same role in many aspects.
This decorator makes it far easier to work with types in Python. This is even more the case when we are working with multiple types that we want to pass through the same method. I wrote more about this in my FuncTools article, so if you are interested in using the single dispatch methodology, I do recommend it (the link is in #1.)
@singledispatchdef fun(arg, verbose=False):        if verbose:            print("Let me just say,", end=" ")        print(arg)@fun.registerdef _(arg: int, verbose=False):    if verbose:        print("Strength in numbers, eh?", end=" ")    print(arg)@fun.registerdef _(arg: list, verbose=False):    if verbose:        print("Enumerate this:")    for i, elem in enumerate(arg):        print(i, elem)
The register function comes from the module atexit. Given that module’s name and M.O., you might be able to imagine that this decorator could have something to do with performing some action at termination. The register decorator names a function to be ran at termination. For example, this would work will with some software that needs to save whenever you exit.
from atexit import register@registerdef termin():    print(" Goodbye!")
Needless to say, Python’s decorators are incredibly useful. Not only can they be used to slow down the time it takes to write some code, but they can also be incredibly helpful at speeding up code. Not only are decorators incredibly useful when you find them about, but it is also a great idea to write your own. These decorators are only powerful because decorators as a feature are very powerful in Python. Thank you for reading my article, and I hope it brought some of Python’s cool abilities to your attention!