Python: Decorators in OOP
The Object Oriented Programming paradigm became popular in the ’60s and ‘70s, in languages like Lisp and Smalltalk. Such features were also added to existing languages like Ada, Fortran and Pascal.
Python is an object oriented programming language, though it doesn’t support strong encapsulation.
Introductory topics in object-oriented programming in Python — and more generally — include things like defining classes, creating objects, instance variables, the basics of inheritance, and maybe even some special methods like __str__. But when we have an advanced look, we could talk about things like the use of decorators, or writing a custom new method, metaclasses, and Multiple Inheritance.
In this post, we’ll first discuss what decorators are, followed by a discussion on classmethods and staticmethods along with the property decorator.
Classmethods, staticmethods and property are examples of what are called descriptors. These are objects which implement the __get__ , __set__ or __delete__ methods.
But, that’s a topic for another post.
We’ll talk about the following in this article:
**- what are decorators? - classmethods - staticmethods - @property**
Let’s work on a simple example: a Student class.
For now, this class has two variables:
We’ll add a simple __init__ method to instantiate an object when these two attributes are provided.
We’ll modify this as we go throughout the post.
Decorators are functions (or classes) that provide enhanced functionality to the original function (or class) without the programmer having to modify their structure.
A simple example?
Suppose we want to add a method to our Student class that takes a student’s score and total marks and then returns a percentage.
Our percent function can be defined like so:
Let’s define our decorator, creatively named record_decorator. It takes a function as input and outputs another function ( wrapper , in this case).
The wrapper function:
- takes our two arguments score and total
- calls the function object passed to the grade_decorator
- then calculates the grade that corresponding to the percent scored.
- Finally, it returns the calculated percentage along with the grade.
We can implement our decorator like so.
Now, to improve the get_percent function, just use the @ symbol with the decorator name above our function, which has exactly the same definition as before.
To use this, we don’t need to modify our call statement. Executing this:
What basically happens is that the function get_percent is replaced by wrapper when we apply the decorator.
We’ll place the get_percent method inside the Student class, and place our decorator outside the class. Since get_percent is an instance method, we add a self argument to it.
How are decorators used in classes?
We’ll see three popular decorators used in classes and their use-cases.
Let’s first talk about instance methods. Instance methods are those methods that are called by an object, and hence are passed information about that object. This is done through the self argument as a convention, and when that method is called, the object’s information is passed implicitly through self.
For example, we could add a method to our class that calculates a student’s grade and percentage (using the get_percent method) and generates a report as a string with the student’s name, percentage, and grade.
Coming to a class method , this type of function is called on a class, and hence, it requires a class to be passed to it. This is done with the cls argument by convention. And we also add the @classmethod decorator to our class method.
It looks something like this:
class A: def instance_method(self): return self **@classmethod def class\_method(cls): return cls** A.class_method()
Since class-methods work with a class, and not an instance, they can be used as part of a factory pattern, where objects are returned based on certain parameters.
For example, there are multiple ways to create a Dataframe in pandas. There are methods like: from_records() , from_dict() , etc. which all return a dataframe object. Though their actual implementation is pretty complex, they basically take something like a dictionary, manipulate that and then return a dataframe object after parsing that data.
Coming back to our example, let's define a few ways to create instances of our Student class.
- by two separate arguments: for example, , 20 and 85
- by a comma-separated string: for example, “, 20, 85”.
- by a tuple: for example, (, 20, 85)
To accomplish this in Java, we could simply overload our constructor:
In python, a clean way to do it would be through classmethods:
We also define the __str__ method, so we can directly print a Student object to see if it has been instantiated properly. Our class now looks like this:
Now, to test this, let’s create three Student objects, each from a different kind of data.
The output is exactly as expected from the definition of the __str__ method above:
Name: John Score: 25 Total : 100 Name: Jack Score: 60 Total : 100 Name: Jill Score: 125 Total : 200
A static method doesn’t care about an instance, like an instance method. It doesn’t require a class being passed to it implicitly.
A static method is a regular method, placed inside a class. It can be called using both a class and an object. We use the @staticmethod decorator for these kinds of methods.
A simple example:
class A: def instance_method(self): return self @classmethod def class_method(cls): return cls **@staticmethod def static\_method(): return** a = A() a.static_method() A.static_method()
Why would this be useful? Why not just place such functions outside the class?
Static methods are used instead of regular functions when it makes more sense to place the function inside the class. For example, placing utility methods that deal solely with a class or its objects is a good idea, since those methods won’t be used by anyone else.
Coming to our example, we can make our get_percent method static, since it serves a general purpose and need not be bound to our objects. To do this, we can simply add @staticmethod above the get_percent method.
The property decorator provides methods for accessing (getter), modifying (setter), and deleting (deleter) the attributes of an object.
Let’s start with getter and setter methods. These methods are used to access and modify (respectively) a private instance. In Java, we would do something like this:
Now, anytime you access or modify this value, you would use these methods. Since the variable x is private, it can’t be accessed outside JavaClass .
In python, there is no private keyword. We prepend a variable by a dunder(__ ) to show that it is private and shouldn’t be accessed or modified directly.
Adding a __ before a variable name modifies that variable’s name from varname to _Classname__varname , so direct access and modification like print(obj.varname) and obj.varname = 5 won’t work. Still, this isn’t very strong since you could directly replace varname with the modified form to get a direct modification to work.
Let’s take the following example to understand this:
Taking our Student class example, let’s make the score attribute “private” by adding a __ before the variable name.
If we directly went ahead and added get_score and set_score like Java, the main issue is that if we wanted to do this to existing code, we’d have to change every access from:
**print("Score: " + str(student1.score))** **student1.score = 100**
Here’s where the @property decorator comes in. You can simply define getter, setter and deleter methods using this feature.
Our class now looks like this:
To make the attribute score read-only, just remove the setter method.
Then, when we update score, we get the following error:
Traceback (most recent call last): File "main.py", line 16, in <module> student.score = 10 **AttributeError: can't set attribute**
The deleter method lets you delete a protected or private attribute using the del function. Using the same example as before, if we directly try and delete the score attribute, we get the following error:
student = Student("Tom", 50, 100) del student.score This gives: Traceback (most recent call last): File "<string>", line 17, in <module> **AttributeError: can't delete attribute**
But when we add a deleter method, we can delete our private variable score .
The attribute has been successfully removed now. Printing out the value of score gives “object has no attribute…”, since we deleted it.
Traceback (most recent call last): File "<string>", line 23, in <module> File "<string>", line 9, in x **AttributeError: 'PythonClass' object has no attribute '\_\_score'**
The property decorator is very useful when defining methods for data validation , like when deciding if a value to be assigned is valid and won’t lead to issues later in the code.
Another use-case would be when wanting to display information in a specific way. Coming back to our example, if we wanted to display a student’s name as “Student Name: ” , instead of just , we could return the first string from a property getter on the name attribute:
Now, any time we access name, we get a formatted result.
student = Student("Bob", 350, 500) **print(student.name)**
**Student Name: Bob**
The property decorator can also be used for logging changes.
For example, in a setter method, you could add code to log the updating of a variable.
Now, whenever the setter is called, which is when the variable is modified, the change is logged. Let’s say there was a totaling error in Bob’s math exam and he ends up getting 50 more marks.
student = Student("Bob", 350, 500) print(student.score) **student.score = 400** print(student.score)
The above gives the following output, with the logged change visible:
70.0 % **INFO:root:Setting new value...** 80.0 %
Finally, our class looks like this:
There are many places you could define a decorator: outside the class, in a separate class, or maybe even in an inner class (with respect to the class we are using the decorator in). In this example, we simply defined grade_decorator outside the Student class. Though this works, the decorator now has nothing to do with our class, which we may not prefer.
For a more detailed discussion on this, check out this post:
Apart from overloading the constructor, we could make use of static factory methods in java. We could define a static method like from_str that would extract key information from the string passed to it and then return an object.
Object-oriented programming is a very important paradigm to learn and use. Regardless of whether you’ll ever need to use the topics discussed here in your next project, it’s necessary to know the basics really well. Topics like the ones in this post aren’t used all that often compared to more basic concepts — like inheritance or the basic implementation of classes and objects — on which they are built. In any case, I hope this post gave you an idea of the other kinds of methods in Python OOP (apart from instance methods) and the property decorator.