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.
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 setter
s 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 setter
s 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}\n' +
62 f'social security number: {self.ssn}\n' +
63 f'gross sales: {self.gross_sales:.2f}\n' +
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).
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
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
(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.
(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.
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.
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'\nbase salary: {self.base_salary:.2f}')
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.
__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.
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.
__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.
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
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
(Fill-In) Function _________ determines whether an object has an “is a” relationship with a specific type.
Answer: isinstance
.
(Fill-In) Function _________ determines whether one class is derived from another.
Answer: issubclass
.
(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
.
CommissionEmployee
s and SalariedCommissionEmployee
s PolymorphicallyWith 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}\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
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).
(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.
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.