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
- Introduction
- Previous post’s challenge’s solution
- Why Do we need class decorators?
- Decorators as classes
- Full code
- Conclusion
- Challenge
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👋