Property
TL;DR
- Use
@propertydecorator for getters and setter in OOP in a more pythonic way.
Python programming provides us with a built-in @property decorator which makes usage of getter and setters much easier in Object-Oriented Programming. We will learn it step by step with an example.
The Temperature Example
Let us assume that we decide to make a class that stores the temperature in degrees Celsius. And, it would also implement a method to convert the temperature into degrees Fahrenheit.
The first naive solution is as follows:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
>>> human = Celsius()
>>> human.temperature = 37
>>> human.temperature
37
>>> human.to_fahrenheit()
98.60000000000001
whenever we assign or retrieve any object attribute like temperature as shown above, Python searches it in the object’s built-in __dict__ dictionary attribute as
print(human.__dict__)
# Output: {'temperature': 37}
Therefore, human.temperature internally becomes human.__dict__['temperature'].
Suppose we want to extend the usability of the Celsius class defined above. We know that the temperature of any object cannot reach below -273.15 degrees Celsius. An obvious solution to the above restriction will be to hide the attribute temperature (make it private) and define new getter and setter methods to manipulate it:
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter method
def get_temperature(self):
return self._temperature
# setter method
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
>>> human = Celsius()
>>> print(human.get_temperature())
37
>>> print(human.to_fahrenheit())
98.60000000000001
>>> human.set_temperature(-300)
Traceback (most recent call last):
File "<string>", line 30, in <module>
File "<string>", line 16, in set_temperature
ValueError: Temperature below -273.15 is not possible.
However, the bigger problem with the above update is that all the programs that implemented our previous class have to modify their code from obj.temperature to obj.get_temperature() and all expressions like obj.temperature = val to obj.set_temperature(val). This refactoring can cause problems while dealing with hundreds of thousands of lines of codes!!!
→ All in all, our new update was not backwards compatible.
The Property class
A pythonic way to deal with the above problem is to use the property class.
We can update our code:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Getting value...")
return self._temperature
# setter
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
# creating a property object
temperature = property(get_temperature, set_temperature)
The last line of the code makes a property object temperature. Property attaches some code (get_temperature and set_temperature) to the member attribute accesses (temperature).
>>> human = Celsius(37)
Setting value...
>>> print(human.temperature)
Getting value...
37
>>> print(human.to_fahrenheit())
Getting value...
98.60000000000001
>>> human.temperature = -300
Setting value...
Traceback (most recent call last):
File "<string>", line 31, in <module>
File "<string>", line 18, in set_temperature
ValueError: Temperature below -273 is not possible
Any code that retrieves the value of temperature (including self.temperature) will automatically call get_temperature() instead of a dictionary (dict) look-up. Similarly, any code that assigns a value to temperature will automatically call set_temperature().
By using property, we can see that NO modification is required in the implementation of the value constraint. Thus, our implementation is backward compatible. 👏
_temperature variable. The temperature attribute is a property object which provides an interface to this private variable.More details
In Python, property() is a built-in function that creates and returns a property object. The syntax of this function is:
property(fget=None, fset=None, fdel=None, doc=None)
Here,
fgetis function to get value of the attributefsetis function to set value of the attributefdelis function to delete the attributedocis a string (like a comment)
A property object has three methods, getter(), setter(), and deleter() to specify fget, fset and fdel at a later point. This means, the line:
temperature = property(get_temperature,set_temperature)
can be broken down as:
# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)
The @property Decorator
A more python (and also the more recommended) way is to use the @property decorator. We can even not define the names get_temperature and set_temperature as they are unnecessary and pollute the class namespace. For this, we reuse the temperature name while defining our getter and setter functions.
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
@property
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter
def temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273 is not possible")
self._temperature = value