Hello again! In this article, we’ll be taking a look at Python property decorator.
Python has a very useful feature called decorators, which is just a syntactic sugar for function-wrappers. Here, we’ll be focusing on the property decorator, which is a special type of decorator.
This topic may be a bit confusing to you, so we’ll cover it step-by-step, using illustrative examples. Let’s get started!
What is the Python Property Decorator based on?
The Property decorator is based on the in-built property()
function. This function returns a special property
object.
You can call this in your Python Interpreter and take a look:
>>> property()
<property object at 0x000002BBD7302BD8>
This property
object has some extra methods, for getting and setting the values of the object. It also has a method for deleting it.
The list of methods is given below:
property().getter
property().setter
property().deleter
But it does not stop there! These methods can be used on other objects too, and act as decorators themselves!
So, for example, we can use property().getter(obj)
, which will give us another property object!
So, the thing to note is that the property decorator will use this function, which will have some special methods for reading and writing to the object. But how does that help us?
Let’s take a look now.
Using the Python Property Decorator
To use the property decorator, we need to wrap it around any function / method.
Here’s a simple example:
@property
def fun(a, b):
return a + b
This is the same as:
def fun(a, b):
return a + b
fun = property(fun)
So here, we wrap property()
around fun()
, which is exactly what a decorator does!
Let’s now take a simple example, by using the property decorator on a class method.
Consider the below class, without the any decorated methods:
class Student():
def __init__(self, name, id):
self.name = name
self.id = id
self.full_id = self.name + " - " + str(self.id)
def get_name(self):
return self.name
s = Student("Amit", 10)
print(s.name)
print(s.full_id)
# Change only the name
s.name = "Rahul"
print(s.name)
print(s.full_id)
Output
Amit
Amit - 10
Rahul
Amit - 10
Here, as you can see, when we only change the name
attribute of our object, it’s reference to the full_id
attribute is still not updated!
To ensure that the full_id
attribute also gets updated whenever name
or id
gets updated, one solution could be to make full_id
into a method instead.
class Student():
def __init__(self, name, id):
self.name = name
self.id = id
def get_name(self):
return self.name
# Convert full_id into a method
def full_id(self):
return self.name + " - " + str(self.id)
s = Student("Amit", 10)
print(s.name)
# Method call
print(s.full_id())
s.name = "Rahul"
print(s.name)
# Method call
print(s.full_id())
Output
Amit
Amit - 10
Rahul
Rahul - 10
Here, we have solved our problem by converting full_id
into a method full_id()
.
However, this is not the best way to tackle this problem, since you may need to convert all such attributes into a method instead, and change the attributes into method calls. This is not convenient!
To reduce our pain, we can use the @property
decorator instead!
The idea is to make full_id()
into a method, but enclose it using @property
. This way, we would be able to update the full_id
, without having to treat it as a function call.
We can directly do this : s.full_id
. Notice that there is no method call here. This is because of the property decorator.
Let’s try this out now!
class Student():
def __init__(self, name, id):
self.name = name
self.id = id
def get_name(self):
return self.name
@property
def full_id(self):
return self.name + " - " + str(self.id)
s = Student("Amit", 10)
print(s.name)
# No more method calls!
print(s.full_id)
s.name = "Rahul"
print(s.name)
# No more method calls!
print(s.full_id)
Output
Amit
Amit - 10
Rahul
Rahul - 10
Indeed, this now works! Now, we don’t need to call full_id
using the parenthesis.
While it is still a method, the property decorator masks that, and treats it as if it is a property of the class! Doesn’t the name make sense now!?
Using property with a setter
In the above example, the approach worked because we didn’t explicitly modify the full_id
property directly. By default, using @property
makes that property only read-only.
This means that you can’t explicitly change the property.
s.full_id = "Kishore"
print(s.full_id)
Output
---> 21 s.full_id = "Kishore"
22 print(s.full_id)
AttributeError: can't set attribute
Obviously, we don’t have the permissions, since the property is read-only!
To make the property writable, remember the property().setter
method we talked about, which is also a decorator?
Turns out we can add another full_id
property using @full_id.setter
, to make it writable. The @full_id.setter
will inherit everything about the original full_id
property, so we can add it directly!
However, we cannot directly use the full_id
property in our setter property. Notice that it will lead to an infinite recursion descent!
class Student():
def __init__(self, name, id):
self.name = name
self.id = id
def get_name(self):
return self.name
@property
def full_id(self):
return self.name + " - " + str(self.id)
@full_id.setter
def full_id(self, value):
# Infinite recursion depth!
# Notice that you're calling the setter property of full_id() again and again
self.full_id = value
To avoid this, we’ll be adding a hidden attribute _full_id
to our class. We’ll modify the @property
decorator to return this attribute instead.
The updated code will now look like this:
class Student():
def __init__(self, name, id):
self.name = name
self.id = id
self._full_id = self.name + " - " + str(self.id)
def get_name(self):
return self.name
@property
def full_id(self):
return self._full_id
@full_id.setter
def full_id(self, value):
self._full_id = value
s = Student("Amit", 10)
print(s.name)
print(s.full_id)
s.name = "Rahul"
print(s.name)
print(s.full_id)
s.full_id = "Kishore - 20"
print(s.name)
print(s.id)
print(s.full_id)
Output
Amit
Amit - 10
Rahul
Amit - 10
Rahul
10
Kishore - 20
We’ve successfully made the full_id
property have getter and setter attributes!
Conclusion
Hopefully, this gave you a better understanding of the property decorator, and also why you may need to use class attributes, like _full_id
.
These hidden class attributes (like _full_id
) make it really easy for us to use the outer full_id
properties!
This is exactly why properties are heavily used in modern open-source projects.
They make it extremely easy for the end-user, and also make it easy for the developers to segregate hidden attributes from non-hidden properties!
References
- StackOverflow Question on Property Decorators