Python Class Decorators: How Fast Does Your Code Run

Hello Pythonistas🙋‍♀️, welcome back. In this post, we are going to discuss class decorators✨ in python.

I mentioned earlier that decorators can be functions or classes and also that they can be used on functions or classes.

We saw:

  • decorators as functions,
  • decorators on functions, and
  • decorators in class.

In the previous post.

Now let’s see:

  • decorators as classes

By creating a decorator that tells the execution time⏱ of your function…

Content

Previous post’s challenge’s solution

Here’s the solution 🧪 to the challenge provided in the last post:

def factory(task_name):
    """
    Factory function that returns a decorator for adding a notification message after executing a task.
    
    Args:
        task_name (str): The name of the task to be displayed in the notification message.
    
    Returns:
        function: The notify decorator.
    """
    def notify(fx):
        """
        Decorator that adds a notification message after executing the decorated function.
        
        Args:
            fx: The function to be decorated.
        
        Returns:
            function: The wrapper function that includes the notification message.
        """
        def wrapper(*args, **kwargs):
            """
            Wrapper function that executes the decorated function and prints the notification message.
            
            Args:
                *args: Variable-length argument list.
                **kwargs: Arbitrary keyword arguments.
            """
            fx(*args, **kwargs)
            print(f"{task_name} checked!")
        return wrapper
    return notify


@factory("Initial Planning")
def initial_planning():
    print("Determine the wedding date and time.")
    print("Set a budget.")
    print("Create a preliminary guest list.")
    print("Decide on the overall wedding style and theme.")

@factory("Selecting Venue And Vendor")
def venue_and_vendor():
    print("\n")
    print("Select venue and vendor based on planning.")

@factory("Decorate the Venue")
def decorate():
    print("\n")
    print("Decorate the venue based on theme.")

@factory("Send Invitations to guests.")
def invite():
    print("\n")
    print("Invite guests.")

@factory("Reharse a day before to ensure no mistakes on the day of wedding.")
def wedding_reharsal():
    print("\n")
    print("Reharse a day before the wedding.")

# Execution of the tasks
initial_planning()
venue_and_vendor()
decorate()
invite()
wedding_reharsal()

Please read the comments in the code to understand it clearly.👍

If you still have any doubts ask them in the comment section below.👇

Why Do we need class decorators?

The first question that might arise🤔 is why we even need to implement decorators with classes when we can already implement them with function.🤷‍♀️

Well, firstly, it’s important to note that class decorators are not❌ as commonly used as function decorators. Function decorators can handle👍 most scenarios effectively.

However, class decorators become valuable in more complex🕸️ cases.

One☝️ primary use case for class decorators is when working with third-party libraries that provide class interfaces.

These libraries, such as Django, Flask, SQLAlchemy, FastAPI, PyTest, and others, often expect or require the use of class decorators to define and modify behavior within their frameworks.

Class decorators also prove beneficial when you need to modify🛠 the behavior of an entire class rather than individual methods or functions.

They allow you to encapsulate💊 and organize related functionality within a single decorator class, resulting in cleaner and more maintainable code.

Additionally, class decorators excel🎉 in scenarios where you need to implement subclasses with specific modifications or customizations.

Decorators as classes: How fast🐆 is your code

So, we will understand how to make a class decorator by making a decorator that lets us know whether the function runs:

  • great🎊
  • good👌
  • bad👎
  • maybe on an infinite loop😵💱

Now obviously, it wouldn’t be a great and robust decorator. But would help us clearly understand how to make and use class decorators.

Let’s first import the time library which we will be using to get the current time.

import time

Now, let’s make a simple function that adds 10 and 20. (I know its quiet lame)

import time
def add():
    a = 10
    b = 20
    print(a + b)

Next, let’s make a class with the __init__() method that takes a function as input and makes it an instance variable.

import time
class TimeTaken:
    def __init__(self, func):
        self.func = func

To make any class a decorator it needs to have a special __call__() method. This method allows the instance of the class to be invoked as a method with parenthesis.

Calculating Time⏱ Taken by a Function

The class decorator will make the function its instance and the __call__() method would allow this instance to be used as a normal function. The code will make it clear:

class TimeTaken:
    def __init__(self, func):
        self.func = func

    def __call__(self):
        start_time = time.time() #current time before the function executes
        self.func()
        end_time = time.time() #current time after the function executes

        total_time = end_time - start_time #difference to get the total time taken.
        print(total_time)

This __call__() method will:

  • Note the current time
  • Execute the function
  • Again note the current time
  • Finally calculating the total time by taking the difference between both times.

Let’s use it on the add() function:

@TimeTaken
def add():
    a = 10
    b = 20
    print(a + b)

Output:

30
0.0

Great but we want to know if the run time is great, good, bad, or infinity level. To do this we just need to add an if elif ladder to the call method:

def __call__(self):
        start_time = time.time()
        self.func()
        end_time = time.time()
        total_time = end_time - start_time
        print(f"The total time taken is: {total_time}")
        
        if total_time <= 0.1:
            print("Runtime is great!")
        elif total_time <= 1.0:
            print("Runtime is average.")
        elif total_time <= 5.0:
            print("Runtime is bad.")
        else:
            print("Function might be on an infinite loop or taking too long.")

Function with argument

What if instead of printing the sum of 10 and 20 add() took two inputs and gave their sum? (This makes sense now)

@TimeTaken
def add(a,b):
    print(a + b)

add(43,35)

You’ll get this error:

TypeError: TimeTaken.__call__() takes 1 positional argument but 3 were given

To get this right we would need to let the __call__() method and the self.func() take all the possible arguments. (0 to any num (even dictionary)).

We have *args and **kwargs for this:

...
def __call__(self, *args, **kwargs):
        start_time = time.time()
        self.func(*args, **kwargs)
...

This will allow the decorator to be used for any function.

Full code:

import time
class TimeTaken:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        start_time = time.time()
        self.func(*args, **kwargs)
        end_time = time.time()
        total_time = end_time - start_time
        print(f"The total time taken is: {total_time}")

        if total_time <= 0.1:
            print("Runtime is great!")
        elif total_time <= 1.0:
            print("Runtime is average.")
        elif total_time <= 5.0:
            print("Runtime is bad.")
        else:
            print("Function might be on an infinite loop or taking too long.")

@TimeTaken
def add(a,b):
    print(a + b)

add(43,35)

Conclusion

With this, we come to the end🔚 of this post. Hope you had a great time😊🕒 with me exploring class decorators in python.

We saw when are class decorators used. We learned how to make class decorators by making one that calculates the execution time of a function and categorizes it as ‘great🎊,’ ‘average👌,’ ‘bad👎,’ or potentially an infinite loop😵💱.

With this new tool in your python toolbox, I’ll see in the next article where we will explore regular expressions in python.

Till then solve this challenge.

Decorators in python official documentation.

Challenge 🧗‍♀️

As programmers👩‍💻👨‍💻, we always have to deal with others’ code. Your challenge for today is simple. You have to handle exceptions raised✋ by the decorated function gracefully.

Suppose I gave 0 as the denominator to a division function, it should raise zero division error. If I give a string to a division function it should raise a type error.

That’s it.

Refer to this post for an overview.

Happy solving…

Stay happy 😄 and keep coding and do suggest any improvements if there.

Take care and have a great 😊 time I’ll see you soon in the next post…Bye Bye👋

Leave a Reply