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