Object-Oriented Programming Part-3¶
Indices and tables¶
Introduction¶
Looking back at the Battery class, dedicated get
and set
methods were defined to allow retrieving/changing instance data at runtime. For example:
1 2 3 4 5 6 7 | def get_percent_charged(self):
"""Gets the battery's change level as a percentage of full charge"""
return self.percent_charged
def set_percent_charged(self, percent_charged):
"""Sets the battery's charge level as a percentage of full charge"""
self.percent_charged = percent_charged
|
The use of dedicated get
and set
methods is good because:
- The
set
method allows data to be validated prior to changing the internal to the instance data - The
get
method allows formatting data, or generating complex data values on without having to store it - Both methods work in combination to protect internal instance state from corruption
However, it is less natural to use as it can’t be the target of an assignment statement.
You also have to change the function name depending on the operation you are doing (reading or writing).
Wouldn’t it easier if users could use assignment syntax like the following:
>>> my_battery.percent_charged = 50
Rather than doing:
>>> my_bettery.set_percent_charged(50)
And wouldn’t it be easier if users could retrieve information from the instance like the following:
>>> print(my_battery.percent_charged)
Rather than doing
>>> print(my_battery.get_percent_charged())
A core principle of Python is to keep things neat, tidy and concise.
This topic will show you how to clean up the data attribute access in our Battery base class by using properties.
Properties¶
Creating a property in Python is done using the following steps:
- Create a private data attribute to hold the raw instance data. This is most commonly done by prepending a
_
to the start the attribute name.
Tip
Creation of a private attribute is optional. What you call the attribute is up to you. The important criteria is that it can’t be the same name as the getter/setter/deleter methods that will be setup to work with it or you will have a name collision.
- Use the @property decorator to declare one or more of a getter, setter and deleter method.
Note
There is no clean way to make a property using classmethods for getter, setter and deleter methods. Only instance methods can easily be transformed into properties.
Looking back at our Battery class, the conversion to using private instance data attributes looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Battery():
"""A base class from which other batteries are derived."""
# Class variables which are meant to be overridden
# by battery types derived from this class.
# The number of batteries created
num_created = 0
# The size/shape of the battery. For example: AA, AAA, C
_form_factor = "Unknown"
# The chemical composition of the cell that creates
# the electrical charge. For example: Alkaline, NiCd, NiMH, LiPo
_chemistry = "Unknown"
# The cell voltage when considered fully discharged
_discharged_voltage = 0
# The cell voltage when considered fully charged
_charged_voltage = 0
@classmethod
def get_num_created(cls):
"""Returns the count of Battery objects created"""
return cls.num_created
|
Property Getters¶
The getter method is what will retrieve (compute) the property value to return. It need not be restricted to only looking at a single private data attribute, it can also perform a computation based on any other data you desire.
Converting our get
methods into properties is done using the @property decorator. The @property decorator does two things:
- It turns the method it is applied to, into a getter method for a read-only attribute of the same name.
- If the method has a docstring, the @property decorator will make it the docstring of the property.
Note
You must define the getter method before you can define the setter or deleter methods.
For our Battery class, it looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # Methods to get/set characteristics of the battery
@property
def form_factor(self):
"""Gets the battery's form factor"""
return self._form_factor
@property
def chemistry(self):
"""Gets the battery's chemistry"""
return self._chemistry
@property
def serial_number(self):
"""Gets the battery's serial number"""
return self._serial_number
@property
def percent_charged(self):
"""Gets the battery's change level as a percentage of full charge"""
return self._percent_charged
@property
def voltage(self):
"""Converts current charge level to a voltage"""
return None
|
Property Setters¶
The setter method is what will accept the property value, validate it, and store it in a data attribute. Similar to getter methods, it need not be restricted to only writing into one private data attribute, it can also perform other computations and write into any other data attributes you desire.
For our Battery class, only the percent_charged
property is settable, and it looks like the following:
1 2 3 4 | @percent_charged.setter
def percent_charged(self, percent_charged):
"""Sets the battery's charge level as a percentage of full charge"""
self._percent_charged = percent_charged
|
Property Deleters¶
It is possible to delete an attribute using a deleter method. However, it isn’t something you normally need to do.
Our Battery class does not require one, but for completeness, you can define one by marking the a deleter method using the @property.deleter
decorator.
Battery Classes With Properties¶
After converting the Battery class to use properties, the comparison methods simplify to the following:
and numerical special methods simply to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # Comparison special methods
def __eq__(self, other):
"""Test if two batteries are equal in voltage."""
return self.voltage == other.voltage
def __le__(self, other):
"""Test if self has a lower voltage than other."""
return self.voltage <= other.voltage
def __lt__(self, other):
"""Test if self has a lower voltage than other."""
return self.voltage < other.voltage
def __ge__(self, other):
"""Test if self has a higher voltage than other."""
return self.voltage >= other.voltage
def __gt__(self, other):
"""Test if self has a higher voltage than other."""
return self.voltage > other.voltage
|
And the numerical special methods simplify to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # Numeric special methods
def __int__(self):
"""Return an integer voltage value."""
return int(self.voltage)
def __float__(self):
"""Return a floating-point voltage value."""
return float(self.voltage)
def __add__(self, other):
"""Returns the total voltage of the battery plus another numeric"""
return float(self) + float(other)
def __radd__(self, other):
"""Returns the total voltage of the battery plus another numeric"""
return float(self) + float(other)
def __sub__(self, other):
"""Returns the total voltage of the battery minus another numeric"""
return float(self) - float(other)
def __rsub__(self, other):
"""Returns the total voltage of the battery minus another numeric"""
return float(self) - float(other)
|
Finally, our battery subclasses, like the KR6 batter, simplify to the following for the voltage
methods:
1 2 3 4 5 6 7 8 | # Override the get_voltage() method to specialize it to
# this battery
@property
def voltage(self):
# Linearly interpolate between fully discharged and
# fully charged voltages, based on percentage_charged.
delta_voltage = self.charged_voltage - self.discharged_voltage
return self.discharged_voltage + delta_voltage * (self.percent_charged / 100)
|
However, the real power of properties comes when you use them because where we used to have to call the appropriate get
and set
method, we can now just access them directly, as in:
>>> #
>>> # Import the batteries we want to use
>>> #
>>> from KR6 import KR6
>>> from HR14 import HR14
>>> from LR4 import LR4
>>> #
>>> # Make some batteries
>>> #
>>> batt0 = KR6("K7811")
>>> batt1 = HR14("AF4194", 35)
>>> batt2 = LR4("GK245-01", 25)
>>> #
>>> # Query some characteristics
>>> #
>>> batt0.form_factor
'AA'
>>> batt0.chemistry
'NiCd'
>>> batt0.serial_number
'K7811'
>>> batt0.percent_charged
0
>>> batt0.voltage
0.9
>>> #
>>> # Change a property value
>>> #
>>> batt0.percent_charged = 50
>>> batt0.voltage
1.05
>>> batt0.percent_charged = 100
>>> batt0.voltage
1.2
>>> int(batt0)
1
>>> float(batt0)
1.2