back

Your free e-book!

See when it is not worth using Scrum.
"Why Scrum Doesn't Work" Download

Class-Based Decorators – make your Python code classy.

In this article, I’m going to talk about Python decorators, briefly explain what they are and put special emphasis on class-based decorators. In the end, I’ll present a real-life example of using them in my work. Let’s get started!

What is a decorator?

To truly understand what decorators are, you should be familiar with terms like first-class functions or closures. We’re not going to cover them in detail in this article, but essentially a programming language has first-class functions when they can be passed as parameters to other functions.

Consider the code above. Function foo, upon execution, returns another function called bar, which preserves it’s enclosing scope. Such kind of object is called closure. This allows them to be passed to other functions as arguments. Decorator is essentially a function that accepts another function and performs some additional logic – so „extends” or „decorates” the original function.

How does it work?

It’s a simple example of the function-based decorator. Note that @decorator_function used above definition of original_function is exactly the same thing as original_function = decorator_function(original_function). The decorator accepts the original function and may perform some logic before it or afterward. Properties of the inner function persist through this process as explained in case of closures.

This is just a superficial explanation of function-based decorators. We’re going to dive more in-depth into class-based decorators, as they are the main subject of this article.

Class-based decorators and why do I prefer them.

In general, Object-Oriented Programming might look a bit complicated at first glance. Its syntax follows certain specific rules, which functions don’t. But there’s a reason for it, as these rules ultimately make it much more convenient to manage. the code in bigger projects.

The same goes for decorators. While at first, class-based ones might look more complex, they are simply more convenient once you understand them. As you can see below, a class-based decorator is simply a class with 2 dunder methods: __init__() and __call__().

The first gets called upon instantiation of a decorating object and accepts decorator-related parameters. The later gets called when that instantiated object gets executed (since in Python, everything is an object and everything can have its __call__() method defined). It accepts the original function and preserves its properties.

While most experienced Python developers tend to use function-based decorators. For me, class-based ones make the code more readable and nicely separates the definition of a decorator itself, with the logic that wraps original function.

class DecoratorClass:

    def __init__(self, decorator_argument):
        """
        Instantiation of decorator_object, like decorator_object = DecoratorClass(arg)
        :param arg: Argument of decorator.
        """
        self.decorator_argument = decorator_argument

    def __call__(self, original_function):
        """
        Called when the decorator_object gets called, like decorator_closure = decorator_object(original_function).
        Returns actual decorator_closure ready to be executed.
        @decorator syntax executes this step automatically.
        """

        def wrapper(*args, **kwargs):
            # Adds logic before and after the original_function
            print('Logic before')
            result = original_function(*args, **kwargs) # function execution
            print('Logic after')
            print(f'Decorator_argument = {self.decorator_argument}')
		# Any argument you need is still accessible inside the wrapper

            return result
        return wrapper

The whole magic behind the execution of class-based decorators appears to be just as simple as in function-based ones. Below you can see an example of a manual step by step execution and using @decorator syntax. Note that both methods return exactly the same results.

@DecoratorClass(decorator_argument=5)
def original_function2(a, b):
    print(f'This is original function. It has arguments a = {a} and b = {b}')

original_function2(1, 2)

Execution using @decorator syntax

def original_function1(a, b):
    print(f'This is original function. It has arguments a = {a} and b = {b}')

# __INIT__
decorator_object = DecoratorClass(decorator_argument=5)
# __CALL__
decorator_closure = decorator_object(original_function1)
# Closure execution
decorator_closure(1, 2)

Manual step by step execution

What makes decorators so great?

What I love most about decorators is the fact that they support popular Don’t Repeat Yourself rule. In the area, where I found it hardest to fulfill – extending existing methods with redundant logic. Have a class with 20 methods returning some statistics? Want to save results to a CSV file? There you go – write a decorator! Have 20 view functions at your backend and want to assign them to a particular URL? That’s exactly how it’s done in Flask.

Once you’ve got a decorator, it’s backward compatible. Also, it’s trivial to apply and does not require any changes in the executing code. In fact, you could simply write another method that would do the same job. But this would force everyone using your code, to update his code in place of execution. In my project, it appeared to be crucial.

Practical example – @LogException

Assume you’ve got a number of different methods parsing some data, which is likely to be corrupted in some way. This is a very common scenario in the life of a data scientist. What you can do is to write a LogException decorator that handles potential Exceptions by logging them to a file.

Such decorators can be parametrized in whatever way you need. In the example below we’re accepting the name of the log file, the mode in which it should be opened (for example ‘w’ to create a new file or ‘a’ to append to existing one) and optional add_time parameter, which defaults to False.

class LogException:

    def __init__(self, filename, mode, add_time=False):
        self.add_time = add_time

    def __call__(self, parse_method):
        def wrapper(*args, **kwargs):
	    file = open(filename, mode)
            try:
                result = parse_method(*args, **kwargs)
            except Exception as error_msg:
                if self.add_time:
                    error_msg = f"{datetime.datetime.now()}: {error_msg}"
                self.file.write(error_msg)
            else:
                return result
            finally:
                file.close()
        return wrapper
@LogException(filename='error_file', mode='w', add_time=True)
def parse_method():
    ...  # method logic
    return data
Software Engineers - development team

Decoration of the parsing method

Note that there’s nothing stopping you from using more advanced logic in your wrapper functions, like try/except/else/finally blocks or loops. This example also nicely shows how clean and readable class-based decorators are, thanks to the clear separation of __init__() and __call__() logic.

And best of all, this single decorator can be reused in every parsing method you’ve got and doesn’t affect execution code at all! If this post didn’t dispel your doubts, please contact us!


cto - Chris Gibas

Free 30-minute consultation with our CTO

Chris Gibas - our CTO will be happy to discuss your project! Let's talk!

More blog posts
What is app modernization, and when should you consider it?

Idego

What is app modernization, and when should you consider it?

Do you feel like your business applications are no longer enough for your organization? It may be the right time for you to consider app modernization. You can improve your business solutions performance and leverage the most popular technical innovations. From this article, you will learn what it means to modernize applications and when you should do it.   Some companies […]

Digital transformation in business – why do you need this for your company?

How can you make your company more efficient, reduce costs and improve the quality of your customer service? Have you ever heard about digital transformation in business? Move from paper documentation to digital systems, adopt new technologies and optimize your business processes to modernize your organization. You can increase profits and make everyday work easier. Read, to learn more.  How […]

Digital transformation in business – why do you need this for your company?

Idego

UX design for web applications: TOP 5 best practices

Idego

UX design for web applications: TOP 5 best practices

Which elements of UX design in the web application would you define as most important? What is the difference between a functional and a successful web app? These are only some of the questions that you should ask yourself before you start the development of your application. CEach web application has some goal (or goals) — selling products, entertaining or […]

Benefits of Artificial Intelligence in banking

The vital role of Artificial Intelligence in banking solutions development is undeniable. Why are ML and AI so important in this industry? Learn more about the current state of AI in banking and the benefits of using AI in banking software development. Check it out, if you are considering the application of AI in your business. Adopting AI-based solutions enables […]

Benefits of Artificial Intelligence in banking

Idego

Get a free estimation

Need a successful project?