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. Theweight
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.