Working With Class and Method Decorators in Python
Decorators added sweet and convenient syntactic convention to the Python language and made coding more fun and effective in Python. In the two previous guides I showed you how to work with function decorators in Python. In this guide I am going to show you how to apply decorators to methods and classes.
Before You Begin
This is not a beginner guide. Going through the code shown here may not require that much advanced knowledge in Python, but to really understand it in depth you definitely need be an intermediate or advanced Python programmer. Before you move forward please check the following list.
- You must have read the previous two guides on decorators and practiced the code on your own
- You must have strong Object Oriented Programming knowledge in Python
- You should have worked with built-in method decorators in Python
- You must have Python 3 installed on your system
- You should be familiar with the command line programs in your system
Preparing the Environment
To get started coding:
- Create a directory or choose an existing one
- Create a file called dec3.py in the chosen directory
- Start your command line in the directory or cd into it
- Run dec3.py to see that everything is alright with the following command:
$ python3 dec3.py
Difference Between Pure Functions and Methods
I do not want to talk much on this topic as I already expect that you know what these are. The fundamental difference between free functions and methods in Python is that methods are bound to classes/instances of that class, and free functions are not bound to anything.
To keep things simpler for you, in this guide we are going to limit our discussion of decorators to the instance methods. Static methods and class methods are created by decorating them with staticmethod() and classmethod() decorators. Descriptors are also in the play of objects and those things are related to this one. I may talk about descriptors in Python in a later guide.
Decorating an Instance Method
An instance method has an instance passed to it during invocation. We just declare that as the first parameter (usually self). But we do not need to pass argument for that parameter when calling that method on an object. Python itself performs the duty. So, if we want to do anything with the object along with the method we can use that inside our decorator.
Let's say we want to print the class name and the method name on the screen when a method is invoked. We want to create a decorator for that.
def meth_decorator(method):
def method_wrapper(self, *args, **kwargs):
print(self.__class__.__name__ + "::" + method.__name__ + "():")
method(self, *args, **kwargs)
print()
return method_wrapper
class C:
@meth_decorator
def say_hello(self, name):
print("Hello, %s" % name)
c = C()
c.say_hello("Sabuj")
This will output:
C::say_hello():
Hello, Sabuj
Look at the method_wrapper(), I have defined the first parameter as self so that I can catch the calling object and find class off of that. But in your use case if you do not need them then it is safe to skip.
Now, imagine that you have dozens of method in your class and you want to decorate them. You’ll have to do something like this:
class C:
@meth_decorator
def say_hello(self, name):
print("Hello, %s" % name)
@meth_decorator
def say_bye(self, name):
print("Bye, %s" % name)
@meth_decorator
def say_go_fast(self, name):
print("Go fast, %s" % name)
...
...
What if we could decorate the method for decorating all those methods and get rid of the repeated method decorator.
Decorating a Class
As we need to return a function or callable from inside the decorator, we need to return a class from inside a class decorator. But the decorator itself remains a function (though we can do it in another way with class but that may not be intuitive for you and I do not think that way as good a way of doing such things).
Let's define the decorator function and an inner class.
def class_decorator(cls):
class WrapperClass:
def __init__(self, *args, **kwargs):
self.instance = cls(*args, **kwargs)
...
...
The decorator function will be passed a class. Inside the WrapperClass we are creating an instance of the class and storing it in the instance variable instance.
Now, we need to decorate the methods of the class instance. But we do not want to find all the methods, iterate over them, and decorate. Instead, we want to decorate them when they are accessed. To do this we need to define the getattribute() method inside the wrapper class.
def class_decorator(cls):
class WrapperClass:
def __init__(self, *args, **kwargs):
self.instance = cls(*args, **kwargs)
def __getattribute__(self, attrib):
try:
obj = super().__getattribute__(attrib)
return obj
except AttributeError:
# will handle the situation below
pass
obj = self.instance.__getattribute__(attrib)
return obj
Inside the try block we are trying to find the attribute in the parent with the help of super call. In Python 3 you do not need to pass the parent class name and the current instance (that is self).
If that is not found, we try to find that inside the the instance of the class we are going to decorate.
But it is not OK to find in the instance only. We should find it inside the class of the instance too. To do so, let's modify our code.
def class_decorator(cls):
class WrapperClass:
def __init__(self, *args, **kwargs):
self.instance = cls(*args, **kwargs)
def __getattribute__(self, attrib):
try:
obj = super().__getattribute__(attrib)
return obj
except AttributeError:
# will handle the situation below
pass
obj = self.instance.__getattribute__(attrib)
if callable(obj):
return meth_decorator(obj)
else:
return obj
Notice the two lines:
if callable(obj):
return meth_decorator(obj)
else:
return obj
getattribute() does not only find methods, it finds any attribute it can find. So, we needed to check whether that object is callable or not. For simplicity we are just assuming that callable() check is enough here, though there is more to it.
So, our final code should look like the following:
def meth_decorator(method):
def method_wrapper(self, *args, **kwargs):
print(self.__class__.__name__ + "::" + method.__name__ + "():")
method(self, *args, **kwargs)
print()
return method_wrapper
def class_decorator(cls):
class WrapperClass:
def __init__(self, *args, **kwargs):
self.instance = cls(*args, **kwargs)
def __getattribute__(self, attrib):
try:
obj = super().__getattribute__(attrib)
return obj
except AttributeError:
# will handle the situation below
pass
obj = self.instance.__getattribute__(attrib)
if callable(obj):
return meth_decorator(obj)
else:
return obj
return WrapperClass
@class_decorator
class C:
def say_hello(self, name):
print("Hello, %s" % name)
def say_bye(self, name):
print("Bye, %s" % name)
def say_go_fast(self, name):
print("Go fast, %s" % name)
c = C()
c.say_hello("Sabuj")
c.say_go_fast("Justt Arifin")
c.say_bye("Sarker")
Run it to see the following output:
str::say_hello():
Hello, Sabuj
str::say_go_fast():
Go fast, Justt Arifin
str::say_bye():
Bye, Sarker
So, we have a quite perfect method decorator now.
Conclusion
Using decorators is easy and fun, but creating complex decorators is not. You need to practice all the code on your own to understand it, and you should come up with new ideas so that you can experiment on new things. I hope to come up with more complex and advanced things related to decorators in Python in future. Keep coding with Python.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment