Python @property decorator



In this tutorial, we will learn about Python @property decorator, a pythonic way to use getters and setters in object-oriented programming.

Python provides a built-in @property decorator that makes usage of getter and setters much easier in Object-Oriented Programming.

Before going deep into @propety, let us see why we need to use @property.


Class Without Getters and Setters

Let us suppose that we decide to create a class that stores the weight in Kilogram. It would also implement a method to convert the weight into pounds. In the following example, we will create this class.

class Kilogram: 
    def __init__(self, weight = 0):
        self.weight = weight

    def to_pound(self):
        return (self.weight * 2.2046)

Now we can make objects out of this class and manipulate the weight attribute.

class Kilogram: 
    def __init__(self, weight = 0):
        self.weight = weight

    def to_pound(self):
        return (self.weight * 2.2046)

human = Kilogram()

# set the weight
human.weight = 65

# get the weight attribute
print(human.weight)

# get the weight in pound
print(human.to_pound())

Output

65 
143.299

Every time we assign or retrieve any object attribute like weight as shown above, Python searches it in the object's built-in __dict__ dictionary attribute.

>>> human.__dict__
{'weight': 65}

Above, the human.weight internally becomes human.__dict__['weight']


Using Getters and Setters

Let us suppose we want to extend the usability of the Kilogram class defined above. We know that the weight of any object cannot reach below 0 Kilograms.

Let us modify the code of the Kilogram class to implement this new constraint.

A solution to the above restriction will be to make the attribute weight private (hide the attribute) and define a new getter and setter methods to manipulate it.

class Kilogram: 
    def __init__(self, weight = 0):
        self.set_weight(weight)

    def to_pound(self):
        return (self.get_weight() * 2.2046)

    # getter method
    def get_weight(self):
        return self._weight

    # setter method
    def set_weight(self, value):
        if value < 0:
            raise ValueError("Weight below 0 is not possible.")
        self._weight = value

As we can see above, we introduce two methods get_weight() and set_weight().

Also, we replaced weight with _weight. The underscore _ at the beginning is used to denote private variables in Python.

Now, we can use the new implementation.

class Kilogram: 
    def __init__(self, weight = 0):
        self.set_weight(weight)

    def to_pound(self):
        return (self.get_weight() * 2.2046)

    # getter method
    def get_weight(self):
        return self._weight

    # setter method
    def set_weight(self, value):
        if value < 0:
            raise ValueError("Weight below 0 is not possible.")
        self._weight = value


# create a new object
human = Kilogram(65)

# get the weight attribute via getter 
print(human.get_weight())

# get the to_pound() method, get_weight() called by the method itself
print(human.to_pound())

# new constraint implementation
human.set_weight(-30)

# get the to_pound method
print(human.to_pound())

Output

65
143.299

Traceback (most recent call last)
...
ValueError: Weight below 0 is not possible.

As we can see above, the new restriction is now implemented. We are no longer allowed to set the weight below 0 kg.

Note: In Python, private variables do not exist. They are just norms to be followed. And there are no restrictions in the Python language to access private variables.

 >>> human._weight = 70
 >>> human.get_weight()
 70

However, there is an issue with the above solution is that all the programs that implemented the Kilogram class have to change their code from obj.weight to obj.get_weight() and all expressions like obj.weight = val to obj.set_weight(val).

The change can be heavy when dealing with hundreds of thousands of lines of codes.

The solution above is not backward compatible. This is where @property comes to help.


The property Class

A pythonic way to deal with the above issue is to use the property class.

In the following code, the update of our code.

class Kilogram: 
    def __init__(self, weight = 0):
        self.weight = weight

    def to_pound(self):
        return (self.weight * 2.2046)

    # getter method
    def get_weight(self):
        print("getting value")
        return self._weight

    # setter method
    def set_weight(self, value):
        print("setting value")
        if value < 0:
            raise ValueError("Weight below 0 is not possible.")
        self._weight = value

    # creating a property object 
    weight = property(get_weight, set_weight)

In the above code, we added a print() function inside get_weight() and set_weight() to see if those functions are being executed.

We also added a property object weight. Property attaches some code (get_weight and set_weight) to the member attribute accesses (weight).

Now let us use this code:

# using property class
class Kilogram: 
    def __init__(self, weight = 0):
        self.weight = weight

    def to_pound(self):
        return (self.weight * 2.2046)

    # getter method
    def get_weight(self):
        print("getting value")
        return self._weight

    # setter method
    def set_weight(self, value):
        print("setting value")
        if value < 0:
            raise ValueError("Weight below 0 is not possible.")
        self._weight = value

    # creating a property object 
    weight = property(get_weight, set_weight)

human = Kilogram(65)

print(human.weight)

print(human.to_pound())

human.weight = -1

Output

setting value
getting value
65
getting value
143.299
setting value

Traceback (most recent call last)
...
ValueError: Weight below 0 is not possible.

As we can see, any code that needs to retrieve the value of weight will automatically call get_weight() instead of a dictionary __dict__ look-up. Similarly, any code that needs to assign a value to weight will automatically call set_weight().

In addition, we can see above that set_weight() was called even when we created an object.

>>> human = Kilogram(65)
setting value

The reason is that when an object is created, the __init__() method gets called. This method has the line self.weight = weight which automatically calls set_weight().

Similarly, any access like human.weight automatically calls get_weight(). This is what property does. Let us see more examples.

>>> human.weight
getting value
65

>>> human.weight = 80
setting value

>>> human.to_pound()
getting value
176.368

By using `property, our implementation is backward compatible. So no modification is required in the implementation of the value constraint.

Note: The actual weight value is stored in the private _weight variable. The weight attribute is a property object that provides an interface to this private variable.


The @property Decorator

In Python, property() is a built-in function that creates and returns a property object.

The syntax of the property function can be given as follows:

property(fget=None, fset=None, fdel=None, doc=None)
  • fget is a function to get the value of the attribute.
  • fset is a function to set the value of the attribute.
  • fdel is a function to delete the attribute.
  • doc is a string (like a comment).

As seen from the implementation, the above function arguments are optional. So, a property object can be created as follows.

>>> property()
<property at 0x7ff406594170>

A property object has three methods, getter(), setter(), and deleter() to specify fget, fste and fdel after the creation of the property. So the following line:

human = property(get_weight, set_weight)

The above line is equivalent to:

# make empty property 
weight = property()

# assign fget
weight = weight.getter(get_weight)

# assign fset
weight = weight.setter(set_weight)

Using Python Decorators the above construct can be implemented as decorators.

We can also not define the names get_weight and set_weight as they are unnecessary.

To do this, we can reuse the weight name while defining our getter and setter functions.

Let us see how to implement this as a decorator:

# using @property decorator
class Kilogram: 
    def __init__(self, weight = 0):
        self.weight = weight

    def to_pound(self):
        return (self.weight * 2.2046)

    @property
    def weight(self):
        print("getting value")
        return self._weight

    @weight.setter
    def weight(self, value):
        print("setting value")
        if value < 0:
            raise ValueError("Weight below 0 is not possible.")
        self._weight = value

# create an object
human = Kilogram(65)

print(human.weight)

print(human.to_pound())

no_human = Kilogram(-10)

Output

setting value
getting value
65
getting value
143.299
setting value

Traceback (most recent call last)
...
ValueError: Weight below 0 is not possible.

As we can see above, the implementation using @property is efficient and straightforward.



ExpectoCode is optimized for learning. Tutorials and examples are constantly reviewed to avoid errors, but we cannot warrant full correctness of all content. While using this site, you agree to have read and accepted our terms of use, cookie and privacy policy.
Copyright 2020-2021 by ExpectoCode. All Rights Reserved.