10.8 Building an Inheritance Hierarchy; Introducing Polymorphism

Let’s use a hierarchy containing types of employees in a company’s payroll app to discuss the relationship between a base class and its subclass. All employees of the company have a lot in common, but commission employees (who will be represented as objects of a base class) are paid a percentage of their sales, while salaried commission employees (who will be represented as objects of a subclass) receive a percentage of their sales plus a base salary.

First, we present base class CommissionEmployee. Next, we create a subclass SalariedCommissionEmployee that inherits from class CommissionEmployee. Then, we use an IPython session to create a SalariedCommissionEmployee object and demonstrate that it has all the capabilities of the base class and the subclass, but calculates its earnings differently.

10.8.1 Base Class CommissionEmployee

Consider class CommissionEmployee, which provides the following features:

  • Method __init__ (lines 8–15), which creates the data attributes _first_name, _last_name and _ssn (Social Security number), and uses the setters of properties gross_sales and commission_rate to create their corresponding data attributes.

  • Read-only properties first_name (lines 17–19), last_name (lines 21–23) and ssn (line 25–27), which return the corresponding data attributes.

  • Read-write properties gross_sales (lines 29–39) and commission_rate (lines 41–52), in which the setters perform data validation.

  • Method earnings (lines 54–56), which calculates and returns a CommissionEmployee’s earnings.

  • Method __repr__ (lines 58–64), which returns a string representation of a CommissionEmployee.

    1 # commmissionemployee.py
    2 """CommissionEmployee base class."""
    3 from decimal import Decimal
    4
    5 class CommissionEmployee:
    6     """An employee who gets paid commission based on gross sales."""
    7
    8     def __init__(self, first_name, last_name, ssn,
    9                  gross_sales, commission_rate):
    10        """Initialize CommissionEmployee's attributes."""
    11        self._first_name = first_name
    12        self._last_name = last_name
    13        self._ssn = ssn
    14        self.gross_sales = gross_sales # validate via property
    15        self.commission_rate = commission_rate # validate via property
    16
    17    @property
    18    def first_name(self):
    19        return self._first_name
    20
    21    @property
    22    def last_name(self):
    23        return self._last_name
    24
    25    @property
    26    def ssn(self):
    27        return self._ssn
    28
    29    @property
    30    def gross_sales(self):
    31        return self._gross_sales
    32
    33    @gross_sales.setter
    34    def gross_sales(self, sales):
    35        """Set gross sales or raise ValueError if invalid."""
    36        if sales < Decimal('0.00'):
    37            raise ValueError('Gross sales must be >= to 0')
    38
    39        self._gross_sales = sales
    40
    41    @property
    42    def commission_rate(self):
    43        return self._commission_rate
    44
    45    @commission_rate.setter
    46    def commission_rate(self, rate):
    47        """Set commission rate or raise ValueError if invalid."""
    48        if not (Decimal('0.0') < rate < Decimal('1.0')):
    49            raise ValueError(
    50               'Interest rate must be greater than 0 and less than 1')
    51
    52        self._commission_rate = rate
    53
    54    def earnings(self):
    55        """Calculate earnings."""
    56        return self.gross_sales * self.commission_rate
    57
    58    def __repr__(self):
    59        """Return string representation for repr()."""
    60        return ('CommissionEmployee: ' +
    61            f'{self.first_name} {self.last_name}
    ' +
    62            f'social security number: {self.ssn}
    ' +
    63            f'gross sales: {self.gross_sales:.2f}
    ' +
    64            f'commission rate: {self.commission_rate:.2f}')
    

Properties first_name, last_name and ssn are read-only. We chose not to validate them, though we could have. For example, we could validate the first and last names—perhaps by ensuring that they’re of a reasonable length. We could validate the Social Security number to ensure that it contains nine digits, with or without dashes (for example, to ensure that it’s in the format ###-##-#### or #########, where each # is a digit).

All Classes Inherit Directly or Indirectly from Class object

You use inheritance to create new classes from existing ones. In fact, 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. The Python class hierarchy begins with class object, the direct or indirect base class of every class. So, 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 or a comma-separated list of base classes for multiple inheritance. Once again, multiple inheritance is beyond the scope of this book.

Class CommissionEmployee inherits all the methods of class object. Class object does not have any data attributes. 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__ (lines 58–64) overrides the default implementation inherited into class CommissionEmployee from class object.7

Testing Class CommissionEmployee

Let’s quickly test some of CommissionEmployee’s features. First, create and display a CommissionEmployee:

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

Next, let’s calculate and display the CommissionEmployee’s earnings:

In [5]: print(f'{c.earnings():,.2f}')
600.00

Finally, let’s 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

tick mark Self Check

  1. (Fill-In) When a base-class method implementation is inappropriate for a derived class, that method can be _________ (i.e., redefined) in the derived class with an appropriate implementation.
    Answer: overridden.

  2. (What Does This Code Do?) In this section’s IPython session, explain in detail what snippet [6] does:

    c.gross_sales = Decimal('20000.00')
    

    This statement creates a Decimal object and assigns it to a CommissionEmployee’s gross_sales property, invoking the property’s setter. The setter checks whether the new value is less than Decimal('0.00'). If so, the setter raises a ValueError, indicating that the value must be greater than or equal to 0; otherwise, the setter assigns the new value to the CommissionEmployee’s _gross_sales attribute.

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. To create class SalariedCommissionEmployee without using inheritance, we could have copied class CommissionEmployee’s code and pasted it into class SalariedCommissionEmployee. Then we could have modified the new class to include a base salary data attribute, and the properties and methods that manipulate the base salary, including a new earnings method. This copy-and-paste approach is often error-prone. Worse yet, it can spread many physical copies of the same code (including errors) throughout a system, making your code less maintainable. Inheritance enables us to “absorb” the features of a class without duplicating code. Let’s see how.

Declaring Class SalariedCommissionEmployee

We now declare the subclass SalariedCommissionEmployee, which inherits most of its capabilities from class CommissionEmployee (line 6). A SalariedCommissionEmployee is a CommissionEmployee (because inheritance passes on the capabilities of class CommissionEmployee), but class SalariedCommissionEmployee also has the following features:

  • Method __init__ (lines 10–15), which initializes all the data inherited from class CommissionEmployee (we’ll say more about this momentarily), then uses the base_salary property’s setter to create a _base_salary data attribute.

  • Read-write property base_salary (lines 17–27), in which the setter performs data validation.

  • A customized version of method earnings (lines 29–31).

  • A customized version of method __repr__ (lines 33–36).

 1 # salariedcommissionemployee.py
 2 """SalariedCommissionEmployee derived from CommissionEmployee."""
 3 from commissionemployee import CommissionEmployee
 4 from decimal import Decimal
 5
 6 class SalariedCommissionEmployee(CommissionEmployee):
 7     """An employee who gets paid a salary plus
 8     commission based on gross sales."""
 9
10     def __init__(self, first_name, last_name, ssn,
11                  gross_sales, commission_rate, base_salary):
12         """Initialize SalariedCommissionEmployee's attributes."""
13         super().__init__(first_name, last_name, ssn,
14                          gross_sales, commission_rate)
15         self.base_salary = base_salary # validate via property
16
17     @property
18     def base_salary(self):
19         return self._base_salary
20
21     @base_salary.setter
22     def base_salary(self, salary):
23         """Set base salary or raise ValueError if invalid."""
24         if salary < Decimal('0.00'):
25            raise ValueError('Base salary must be >= to 0')
26
27         self._base_salary = salary
28
29     def earnings(self):
30         """Calculate earnings."""
31         return super().earnings() + self.base_salary
32
33     def __repr__(self):
34         """Return string representation for repr()."""
35         return ('Salaried' + super().__repr__() +
36             f'
base salary: {self.base_salary:.2f}')

Inheriting from Class CommissionEmployee

To inherit from a class, you must first import its definition (line 3). Line 6

class SalariedCommissionEmployee(CommissionEmployee):

specifies that class SalariedCommissionEmployee inherits from CommissionEmployee. Though you do not see class CommissionEmployee’s data attributes, properties and methods in class SalariedCommissionEmployee, they’re nevertheless part of the new class, as you’ll soon see.

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. SalariedCommissionEmployee’s __init__ method explicitly calls class CommissionEmployee’s __init__ method (lines 13–14) to initialize the base-class portion of a SalariedCommissionEmployee object (that is, the five inherited data attributes from class CommissionEmployee). The notation super().__init__ uses the built-in function super to locate and call the base class’s __init__ method, passing the five arguments that initialize the inherited data attributes.

Overriding Method earnings

Class SalariedCommissionEmployee’s earnings method (lines 29–31) overrides class CommissionEmployee’s earnings method (Section 10.8.1, lines 54–56) to calculate the earnings of a SalariedCommissionEmployee. The new version obtains the portion of the earnings based on commission alone by calling CommissionEmployee’s earnings method with the expression super().earnings() (line 31). SalariedCommissionEmployee’s earnings method then adds the base_salary to this value to calculate the total earnings. By having SalariedCommissionEmployee’s earnings method invoke CommissionEmployee’s earnings method to calculate part of a SalariedCommissionEmployee’s earnings, we avoid duplicating the code and reduce code-maintenance problems.

Overriding Method __repr__

SalariedCommissionEmployee’s __repr__ method (lines 33–36) overrides class CommissionEmployee’s __repr__ method (Section 10.8.1, lines 58–64) to return a String representation that’s appropriate for a SalariedCommissionEmployee. The subclass creates part of the string representation by concatenating 'Salaried' and the string returned by super().__repr__(), which calls CommissionEmployee’s __repr__ method. The overridden method then concatenates the base salary information and returns the resulting string.

Testing Class SalariedCommissionEmployee

Let’s test class SalariedCommissionEmployee to show that it indeed inherited capabilities from class CommissionEmployee. First, let’s create a SalariedCommissionEmployee and print all of its properties:

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

Notice that the SalariedCommissionEmployee object has all of the properties of classes CommissionEmployee and SalariedCommissionEmployee.

Next, let’s calculate and display the SalariedCommissionEmployee’s earnings. Because we call method earnings on a SalariedCommissionEmployee object, the subclass version of the method executes:

In [12]: print(f'{s.earnings():,.2f}')
500.00

Now, let’s 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

Again, because this method is called on a SalariedCommissionEmployee object, the subclass version of the method executes. Finally, let’s calculate and display the SalariedCommissionEmployee’s updated earnings:

In [17]: print(f'{s.earnings():,.2f}')
1,500.00

Testing the “is a” Relationship

Python provides two built-in functions—issubclass and isinstance—for testing “is a” relationships. Function issubclass determines whether one class is derived from another:

In [18]: issubclass(SalariedCommissionEmployee, CommissionEmployee)
Out[18]: True

Function isinstance determines whether an object has an “is a” relationship with a specific type. Because SalariedCommissionEmployee inherits from CommissionEmployee, both of the following snippets return True, confirming the “is a” relationship

In [19]: isinstance(s, CommissionEmployee)
Out[19]: True

In [20]: isinstance(s, SalariedCommissionEmployee)
Out[20]: True

tick mark Self Check

  1. (Fill-In) Function _________ determines whether an object has an “is a” relationship with a specific type.
    Answer: isinstance.

  2. (Fill-In) Function _________ determines whether one class is derived from another.
    Answer: issubclass.

  3. (What Does This Code Do?) Explain in detail what the following statement from class SalariedCommissionEmployee’s earnings method does:

    return super().earnings() + self.base_salary
    

    This statement calculates a SalariedCommissionEmployee’s earnings by using the built-in function super to invoke the base class CommissionEmployee’s version of method earnings then adding to the result the base_salary.

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. We can take advantage of this “subclass-object-is-a-base-class-object” relationship to perform some interesting manipulations. For example, we can place objects related through inheritance into a list, then iterate through the list and treat each element as a base-class object. This allows a variety of objects to be processed in a general way. Let’s demonstrate this by placing the CommissionEmployee and SalariedCommissionEmployee objects in a list, then for each element displaying its string representation and earnings:

In [21]: employees = [c, s]
In [22]: for employee in employees:
    ...: print(employee)
    ...: print(f'{employee.earnings():,.2f}
')
    ...:
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

As you can see, the correct string representation and earnings are displayed for each employee. This is called polymorphism—a key capability of object-oriented programming (OOP).

tick mark Self Check

  1. (Fill-In) _________ enables us to take advantage of the “subclass-object-is-a-base-class-object” relationship to process objects in a general way.
    Answer: Polymorphism.

10.8.4A Note About Object-Based and Object-Oriented Programming

Inheritance with method overriding is a powerful way to build software components that are like existing components but need to be customized to your application’s unique needs. In the Python open-source world, there are a huge number of well-developed class libraries for which your programming style is:

  • know what libraries are available,

  • know what classes are available,

  • make objects of existing classes, and

  • send them messages (that is, call their methods).

This style of programming called object-based programming (OBP). When you do composition with objects of known classes, you’re still doing object-based programming. Adding inheritance with overriding to customize methods to the unique needs of your applications and possibly process objects polymorphically is called object-oriented programming (OOP). If you do composition with objects of inherited classes, that’s also object-oriented programming.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.57.222