When we talk about decorating classes, we can either decorate individual methods or we can create a decorator to decorate the whole class.
class MyClass: def __init__(self, a): self.a = a @timer def method1(self, x, y): print('method1 executing') @timer def method2(self, x, y): print('method2 executing') @trace def method3(self, x, y): print('method3 executing')
Decorating methods is straightforward, it is like decorating functions only. For example, the decorators trace and timer that we have created in our lectures, can be applied to methods of a class to trace or time the method calls. While applying the decorators to methods of a class, we need to keep in mind that a method always receives the current instance as the first argument. The decorators trace and timer that we had created, will work properly with methods also because they are not doing anything special with the first argument but suppose you have a decorator that sanitizes the first argument of a function then you cannot simply apply that decorator to a method, because for methods, the current instance is the first argument, and the first argument that you send when you call the method is actually the second argument.
Now, let us see how to create a decorator function to decorate the class as a whole. This type of decorator function will take a class as the argument. It will either modify that class and return it, or it will create a new class and return it. Modifying the class in-place and returning the modified class is more convenient than creating a new class. So, we will see some examples where we will create a decorator that takes a class and returns the modified class.
The syntax for applying the decorator to a class will be the same. You can either use the automatic way or decorate the class manually.
@decorator class MyClass: pass class MyClass: pass MyClass = decorator(MyClass)
First, let us create a very simple decorator, which, when applied to a class, adds a new attribute named author and a line to the docstring of the class.
def my_decorator(cls): if cls.__doc__ is None: cls.__doc__ = '\nThis is an important class\n' else: cls.__doc__ += '\nThis is an important class\n' cls.author = 'Ryan' return cls @my_decorator class Person: """This is the docstring of Person class""" def __init__(self, name, age): self.name = name self.age = age def speak(self): print(f'Hello, I am {self.name}') print(Person.__doc__) print(Person.__dict__) class Car: def __init__(self, model, max_speed): self.model = model self.max_speed = max_speed def show(self): print(f'{self.model}, {self.max_speed}') Car = my_decorator(Car) print(Car.__doc__) print(Car.__dict__)
Output-
This is the docstring of Person class
This is an important class
{'__module__': '__main__' , …………… , 'author': 'Ryan'}
This is an important class
{'__module__': '__main__', ……………… , 'author': 'Ryan'}
Our decorator takes in a class as the argument, so we have named its parameter cls. If the docstring of the class is None, then we assign a string to the docstring; otherwise, we add the string to the docstring. After this we add a new attribute named author to the class. At last, we return the class from the decorator.
We have applied this decorator to the two classes named Person and Car. The Person class has been decorated using the automatic syntax, while the class Car has been decorated using the manual decoration syntax. The output clearly shows the effect of decoration.
In our next example, we have created a decorator that adds an attribute named time_of_creation to each instance of the class. Note that in the previous example, we added an attribute to the class object, so we had actually created a class variable. Now, we want to add an attribute to each instance object, so we will be creating an instance variable. This attribute, named time_of_creation, will store the time when the instance object is created. The decorator will also print a message whenever a new instance object is created.
def add_creation_time(cls): init = cls.__init__ def new_init(self,*args, **kwargs): from time import ctime self.time_of_creation = ctime() print(f'A new object of type {cls.__name__ } created') init(self,*args, **kwargs) cls.__init__ = new_init return cls @add_creation_time class Person: def __init__(self, name, age): self.name = name self.age = age def speak(self): print(f'Hello, I am {self.name}') @add_creation_time class Car: def __init__(self, model, color): self.model = model self.color = color def show(self): print(f'{self.model}, {self.max_speed}') bob = Person('Bob', 23) tom = Person('Tom', 66) x = Car('Audi R8', 'White') y = Car('Jaguar XJ', 'Black') print(bob.time_of_creation) print(tom.time_of_creation) print(x.time_of_creation) print(y.time_of_creation)
Output-
A new object of type Person created
A new object of type Person created
A new object of type Car created
A new object of type Car created
Tue Aug 22 16:57:32 2023
Tue Aug 22 16:57:32 2023
Tue Aug 22 16:57:32 2023
Tue Aug 22 16:57:32 2023
Let us understand the code that is written inside the decorator. We have to print the message and add the attribute when an instance is created, so we will have to change the __init__ method. First, we save the __init__ in a separate variable named init. Then we define a function named new_init. The first argument is self and then we place args and kwargs.
After that, we imported the ctime function from the time module. We add a new attribute named time_of_creation. After that, we print the message, that a new object has been created. Next, we call this original __init__ that we have saved in the variable init. The extra work has been done before calling the original init. Then we assign the new_init method to the __init__ attribute of the class. And at last, we return the class object.
We have applied this decorator to the classes Person and Car. The output shows that when instance objects of these classes are created, a message is displayed. A new attribute named time_of_creation gets attached to each instance, and its value is the time when the instance object is created.