10.8 Building an Inheritance Hierarchy; Introducing Polymorphism

  • Hierarchy containing types of employees in a company’s payroll app
  • All employees of the company have a lot in common
    • commission employees (who will be represented as objects of a base class) are paid a percentage of their sales
    • salaried commission employees (who will be represented as objects of a subclass) receive a percentage of their sales plus a base salary

10.8.1 Base Class CommissionEmployee

Class CommissionEmployee provides the following features:

  • Method __init__ creates the data attributes _first_name, _last_name and _ssn (Social Security number), and uses the setter's of properties gross_sales and commission_rate to create their corresponding data attributes
  • Read-only properties first_name, last_name and ssn, which return the corresponding data attributes
  • Read-write properties gross_sales and commission_rate in which the setters perform data validation
  • Method earnings, which calculates and returns a CommissionEmployee’s earnings
  • Method __repr__, which returns a string representation of a CommissionEmployee
# commmissionemployee.py
"""CommissionEmployee base class."""
from decimal import Decimal

class CommissionEmployee:
    """An employee who gets paid commission based on gross sales."""

    def __init__(self, first_name, last_name, ssn, 
                 gross_sales, commission_rate):
        """Initialize CommissionEmployee's attributes."""
        self._first_name = first_name
        self._last_name = last_name
        self._ssn = ssn
        self.gross_sales = gross_sales  # validate via property
        self.commission_rate = commission_rate  # validate via property

    @property
    def first_name(self):
        return self._first_name

    @property
    def last_name(self):
        return self._last_name

    @property
    def ssn(self):
        return self._ssn

    @property
    def gross_sales(self):
        return self._gross_sales

    @gross_sales.setter
    def gross_sales(self, sales):
        """Set gross sales or raise ValueError if invalid."""
        if sales < Decimal('0.00'):
            raise ValueError('Gross sales must be >= to 0')

        self._gross_sales = sales

    @property
    def commission_rate(self):
        return self._commission_rate

    @commission_rate.setter
    def commission_rate(self, rate):
        """Set commission rate or raise ValueError if invalid."""
        if not (Decimal('0.0') < rate < Decimal('1.0')):
            raise ValueError(
               'Interest rate must be greater than 0 and less than 1')

        self._commission_rate = rate

    def earnings(self):
        """Calculate earnings."""   
        return self.gross_sales * self.commission_rate

    def __repr__(self):
        """Return string representation for repr()."""
        return ('CommissionEmployee: ' + 
            f'{self.first_name} {self.last_name}\n' +
            f'social security number: {self.ssn}\n' +
            f'gross sales: {self.gross_sales:.2f}\n' +
            f'commission rate: {self.commission_rate:.2f}')

All Classes Inherit Directly or Indirectly from Class object

  • Every Python class inherits from an existing class
  • When you do not explicitly specify the base class for a new class, Python assumes that the class inherits directly from class object
  • Class CommissionEmployee’s header could have been written as
    class CommissionEmployee(object):
    
  • The parentheses after CommissionEmployee indicate inheritance and may contain
    • a single class for single inheritance
    • a comma-separated list of base classes for multiple inheritance

All Classes Inherit Directly or Indirectly from Class object (cont.)

  • CommissionEmployee inherits all the methods of class object
  • Two of the many methods inherited from object are __repr__ and __str__
    • So every class has these methods that return string representations of the objects on which they’re called
  • When a base-class method implementation is inappropriate for a derived class, that method can be overridden (i.e., redefined) in the derived class with an appropriate implementation
    • Method __repr__ overrides the default implementation from class object

Testing Class CommissionEmployee

  • test some of CommissionEmployee’s features
In [1]:
from commissionemployee import CommissionEmployee
In [2]:
from decimal import Decimal
In [3]:
c = CommissionEmployee('Sue', 'Jones', '333-33-3333', 
    Decimal('10000.00'), Decimal('0.06'))
In [4]:
c
Out[4]:
CommissionEmployee: Sue Jones
social security number: 333-33-3333
gross sales: 10000.00
commission rate: 0.06
  • calculate and display the CommissionEmployee’s earnings
In [5]:
print(f'{c.earnings():,.2f}')
600.00
  • change the CommissionEmployee’s gross sales and commission rate, then recalculate the earnings
In [6]:
c.gross_sales = Decimal('20000.00')
In [7]:
c.commission_rate = Decimal('0.1')
In [8]:
print(f'{c.earnings():,.2f}')
2,000.00

10.8.2 Subclass SalariedCommissionEmployee

  • With single inheritance, the subclass starts essentially the same as the base class
  • The real strength of inheritance comes from the ability to define in the subclass additions, replacements or refinements for the features inherited from the base class.
  • Many of a SalariedCommissionEmployee’s capabilities are similar, if not identical, to those of class CommissionEmployee
    • Both types of employees have first name, last name, Social Security number, gross sales and commission rate data attributes, and properties and methods to manipulate that data
  • Inheritance enables us to “absorb” the features of a class without duplicating code

Declaring Class SalariedCommissionEmployee

  • Subclass SalariedCommissionEmployee inherits most of its capabilities from class CommissionEmployee
  • A SalariedCommissionEmployee is a CommissionEmployee (because inheritance passes on the capabilities of class CommissionEmployee)
  • Class SalariedCommissionEmployee also has the following features:
    • Method __init__, which initializes all the data inherited from class CommissionEmployee, then uses the base_salary property’s setter to create a _base_salary data attribute
    • Read-write property base_salary, in which the setter performs data validation.
    • A customized version of method earnings
    • A customized version of method __repr__
# salariedcommissionemployee.py
"""SalariedCommissionEmployee derived from CommissionEmployee."""
from commissionemployee import CommissionEmployee
from decimal import Decimal

class SalariedCommissionEmployee(CommissionEmployee):
    """An employee who gets paid a salary plus 
    commission based on gross sales."""

    def __init__(self, first_name, last_name, ssn, 
                 gross_sales, commission_rate, base_salary):
        """Initialize SalariedCommissionEmployee's attributes."""
        super().__init__(first_name, last_name, ssn, 
                         gross_sales, commission_rate)
        self.base_salary = base_salary  # validate via property

    @property
    def base_salary(self):
        return self._base_salary

    @base_salary.setter
    def base_salary(self, salary):
        """Set base salary or raise ValueError if invalid."""
        if salary < Decimal('0.00'):
            raise ValueError('Base salary must be >= to 0')

        self._base_salary = salary

    def earnings(self):
        """Calculate earnings."""   
        return super().earnings() + self.base_salary

    def __repr__(self):
        """Return string representation for repr()."""
        return ('Salaried' + super().__repr__() +      
            f'\nbase salary: {self.base_salary:.2f}')

Inheriting from Class CommissionEmployee

class SalariedCommissionEmployee(CommissionEmployee):
  • specifies that class SalariedCommissionEmployee inherits from CommissionEmployee
  • Don't see class CommissionEmployee’s data attributes, properties and methods in class SalariedCommissionEmployee, but they are there

Method __init__ and Built-In Function super

  • Each subclass __init__ must explicitly call its base class’s __init__ to initialize the data attributes inherited from the base class
    • This call should be the first statement in the subclass’s __init__ method
  • The notation super().__init__ uses the built-in function super to locate and call the base class’s __init__ method

Overriding Method earnings

  • Class SalariedCommissionEmployee’s earnings method overrides class CommissionEmployee’s earnings method to calculate the earnings of a SalariedCommissionEmployee
    • Obtains the portion of the earnings based on commission alone by calling CommissionEmployee’s earnings method with the expression super().earnings()

Overriding Method __repr__

  • SalariedCommissionEmployee’s __repr__ method overrides class CommissionEmployee’s __repr__ method to return a String representation that’s appropriate for a SalariedCommissionEmployee
  • super().__repr__() calls CommissionEmployee's __repr__ method

Testing Class SalariedCommissionEmployee

In [9]:
from salariedcommissionemployee import SalariedCommissionEmployee
In [10]:
s = SalariedCommissionEmployee('Bob', 'Lewis', '444-44-4444',
        Decimal('5000.00'), Decimal('0.04'), Decimal('300.00'))
In [11]:
print(s.first_name, s.last_name, s.ssn, s.gross_sales, 
      s.commission_rate, s.base_salary)
Bob Lewis 444-44-4444 5000.00 0.04 300.00
  • SalariedCommissionEmployee object has all of the properties of classes CommissionEmployee and SalariedCommissionEmployee
  • Calculate and display the SalariedCommissionEmployee’s earnings
In [12]:
print(f'{s.earnings():,.2f}')
500.00
  • Modify the gross_sales, commission_rate and base_salary properties, then display the updated data via the SalariedCommissionEmployee’s __repr__ method
In [13]:
s.gross_sales = Decimal('10000.00')
In [14]:
s.commission_rate = Decimal('0.05')
In [15]:
s.base_salary = Decimal('1000.00')
In [16]:
print(s)
SalariedCommissionEmployee: Bob Lewis
social security number: 444-44-4444
gross sales: 10000.00
commission rate: 0.05
base salary: 1000.00
  • Calculate and display the SalariedCommissionEmployee’s updated earnings
In [17]:
print(f'{s.earnings():,.2f}')
1,500.00

Testing the “is a” Relationship

Functions issubclass and isinstance are used to test “is a” relationships

  • issubclass determines whether one class is derived from another
In [18]:
issubclass(SalariedCommissionEmployee, CommissionEmployee)
Out[18]:
True
  • isinstance determines whether an object has an “is a” relationship with a specific type
In [19]:
isinstance(s, CommissionEmployee)
Out[19]:
True
In [20]:
isinstance(s, SalariedCommissionEmployee)
Out[20]:
True

10.8.3 Processing CommissionEmployees and SalariedCommissionEmployees Polymorphically

  • With inheritance, every object of a subclass also may be treated as an object of that subclass’s base class
  • Can take advantage of this relationship to place objects related through inheritance into a list, then iterate through the list and treat each element as a base-class object
    • Allows a variety of objects to be processed in a general way
In [21]:
employees = [c, s]
In [22]:
for employee in employees:
    print(employee)
    print(f'{employee.earnings():,.2f}\n')
CommissionEmployee: Sue Jones
social security number: 333-33-3333
gross sales: 20000.00
commission rate: 0.10
2,000.00

SalariedCommissionEmployee: Bob Lewis
social security number: 444-44-4444
gross sales: 10000.00
commission rate: 0.05
base salary: 1000.00
1,500.00

  • Correct string representation and earnings are displayed for each employee
  • This is called polymorphism—a key capability of object-oriented programming (OOP)

©1992–2020 by Pearson Education, Inc. All Rights Reserved. This content is based on Chapter 5 of the book Intro to Python for Computer Science and Data Science: Learning to Program with AI, Big Data and the Cloud.

DISCLAIMER: The authors and publisher of this book have used their best efforts in preparing the book. These efforts include the development, research, and testing of the theories and programs to determine their effectiveness. The authors and publisher make no warranty of any kind, expressed or implied, with regard to these programs or to the documentation contained in these books. The authors and publisher shall not be liable in any event for incidental or consequential damages in connection with, or arising out of, the furnishing, performance, or use of these programs.