How to Use Python Property Decorator?

Python Property(7)

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