3

Inheritance and Composition: A Python OOP Guide

 7 months ago
source link: https://realpython.com/inheritance-composition-python/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

A Python OOP Guide – Real Python

What Are Inheritance and Composition?

Inheritance and composition are two major concepts in object-oriented programming that model the relationship between two classes. They drive the design of an application and determine how the application should evolve as new features are added or requirements change.

Both of them enable code reuse, but they do it in different ways.

What’s Inheritance?

Inheritance models what’s called an is a relationship. This means that when you have a Derived class that inherits from a Base class, you’ve created a relationship where Derived is a specialized version of Base.

Inheritance is represented using the Unified Modeling Language, or UML, in the following way:

Basic inheritance between Base and Derived classes

This model represents classes as boxes with the class name on top. It represents the inheritance relationship with an arrow from the derived class pointing to the base class. The word extends is usually added to the arrow.

Note: In an inheritance relationship:

Say you have the base class Animal, and you derive from it to create a Horse class. The inheritance relationship states that Horse is an Animal. This means that Horse inherits the interface and implementation of Animal, and you can use Horse objects to replace Animal objects in the application.

This is known as the Liskov substitution principle. The principle states that if S is a subtype of T, then replacing objects of type T with objects of type S doesn’t change the program’s behavior.

You’ll see in this tutorial why you should always follow the Liskov substitution principle when creating your class hierarchies, and you’ll learn about the problems that you’ll run into if you don’t.

What’s Composition?

Composition is a concept that models a has a relationship. It enables creating complex types by combining objects of other types. This means that a class Composite can contain an object of another class Component. This relationship means that a Composite has a Component.

UML represents composition as follows:

Basic composition between Composite and Component classes

The model represents composition through a line that starts with a diamond at the composite class and points to the component class. The composite side can express the cardinality of the relationship. The cardinality indicates the number or the valid range of Component instances that the Composite class will contain.

In the diagram above, the 1 represents that the Composite class contains one object of type Component. You can express cardinality in the following ways:

Note: Classes that contain objects of other classes are usually referred to as composites, while classes that are used to create more complex types are referred to as components.

For example, your Horse class can be composed by another object of type Tail. Composition allows you to express that relationship by saying Horse has a Tail.

Composition enables you to reuse code by adding objects to other objects, as opposed to inheriting the interface and implementation of other classes. Both Horse and Dog classes can leverage the functionality of Tail through composition without deriving one class from the other.

An Overview of Inheritance in Python

Everything in Python is an object. Modules are objects, class definitions and functions are objects, and of course, objects created from classes are objects too.

Inheritance is a required feature of every object-oriented programming language. This means that Python supports inheritance, and as you’ll see later, it’s one of the few languages that supports multiple inheritance.

When you write Python code using classes, you’re using inheritance even if you don’t know that you’re using it. Next up, take a look at what that means.

The Object Super Class

The easiest way to see inheritance in Python is to jump into the Python interactive shell and write a little bit of code. You’ll start by writing the simplest class possible:

Python
>>> class EmptyClass:
...     pass
...

You declared EmptyClass, which doesn’t do much, but it’ll illustrate the most basic inheritance concepts. Now that you have the class declared, you can create an instance of the class and use the dir() function to list its members:

Python
>>> c = EmptyClass()
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

The dir() function returns a list of all the members in the specified object. You haven’t declared any members in EmptyClass, so where’s the list coming from? You can find out using the interactive interpreter:

Python
>>> o = object()
>>> dir(o)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__']

As you can see, the two lists are nearly identical. There are three additional members in EmptyClass:

  1. __dict__
  2. __module__
  3. __weakref__

However, every single member of the object class is also present in EmptyClass.

This is because every class that you create in Python implicitly derives from object. You could be more explicit and write class EmptyClass(object):, but it’s redundant and unnecessary.

Note: In Python 2, you had to explicitly derive from object for reasons beyond the scope of this tutorial, but you can read about it in the new-style and classic classes section of the Python 2 documentation.

Okay, it’s not entirely true that every class in Python derives from object. There’s one aptly named exception, which you’ll learn about next.

Exceptions Are an Exception

Every class that you create in Python will implicitly derive from object. However, there’s one exception to this rule: classes used to indicate errors by raising an exception.

If you try to treat a normal Python class like an exception and raise it, then Python will present you with a TypeError:

Python
>>> class NotAnError:
...     pass
...
>>> raise NotAnError()
Traceback (most recent call last):
  ...
TypeError: exceptions must derive from BaseException

You created a new class to indicate a type of error. Then you tried to raise the class to signal an exception. Python does indeed raise an exception, but the output states that the exception is of type TypeError, not NotAnError, and that all exceptions must derive from BaseException.

BaseException is a base class provided for all error types. To create a new error type, you must derive your class from BaseException or one of its derived classes. The convention in Python is to derive your custom error types from Exception, which in turn derives from BaseException.

The correct way to define your error type is the following:

Python
>>> class AnError(Exception):
...     pass
...
>>> raise AnError()
Traceback (most recent call last):
  ...
AnError

In this example, AnError explicitly inherits from Exception instead of implicitly inheriting from object. With that change, you’ve fulfilled the requirements for creating a custom exception, and you can now raise your new exception class. When you raise AnError, the output correctly states that Python raised an error of the type AnError.

Creating Class Hierarchies

Inheritance is the mechanism that you’ll use to create hierarchies of related classes. These related classes will share a common interface that the base classes will define. Derived classes can specialize the interface by providing a particular implementation where applicable.

In this section, you’ll start modeling an HR system. Along the way, you’ll explore the use of inheritance and see how derived classes can provide a concrete implementation of the base class interface.

The HR system needs to process payroll for the company’s employees, but there are different types of employees depending on how their payroll is calculated.

You start by implementing a PayrollSystem class that processes payroll:

Python hr.py
class PayrollSystem:
    def calculate_payroll(self, employees):
        print("Calculating Payroll")
        print("===================")
        for employee in employees:
            print(f"Payroll for: {employee.id} - {employee.name}")
            print(f"- Check amount: {employee.calculate_payroll()}")
            print("")

PayrollSystem implements a .calculate_payroll() method that takes a collection of employees and prints their .id, .name, and check amount using the .calculate_payroll() method exposed on each employee object.

Now, you implement a base class, Employee, that handles the common interface for every employee type:

Python hr.py
# ...

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

Employee is the base class for all employee types. It’s constructed with an .id and a .name. What you’re saying is that every Employee must have an .id as well as a .name assigned.

The HR system requires that every Employee processed must provide a .calculate_payroll() interface that returns the weekly salary for the employee. The implementation of that interface differs depending on the type of Employee.

For example, administrative workers have a fixed salary, so every week they get paid the same amount:

Python hr.py
# ...

class SalaryEmployee(Employee):
    def __init__(self, id, name, weekly_salary):
        super().__init__(id, name)
        self.weekly_salary = weekly_salary

    def calculate_payroll(self):
        return self.weekly_salary

You create a derived class, SalaryEmployee, that inherits from Employee. The class initializes with the .id and .name required by the base class, and you use super() to initialize the members of the base class. You can read all about super() in Supercharge Your Classes With Python super().

SalaryEmployee also requires a weekly_salary initialization parameter that represents the amount that the employee makes per week.

The class provides the required .calculate_payroll() method that the HR system uses. The implementation just returns the amount stored in weekly_salary.

The company also employs manufacturing workers who are paid by the hour, so you add HourlyEmployee to the HR system:

Python hr.py
# ...

class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

The HourlyEmployee class is initialized with .id and .name, like the base class, plus the hours_worked and the hourly_rate required to calculate the payroll. You implement the .calculate_payroll() method by returning the hours worked times the hourly rate.

Finally, the company employs sales associates who are paid through a fixed salary plus a commission based on their sales, so you create a CommissionEmployee class:

Python hr.py
# ...

class CommissionEmployee(SalaryEmployee):
    def __init__(self, id, name, weekly_salary, commission):
        super().__init__(id, name, weekly_salary)
        self.commission = commission

    def calculate_payroll(self):
        fixed = super().calculate_payroll()
        return fixed + self.commission

You derive CommissionEmployee from SalaryEmployee because both classes have a weekly_salary to consider. At the same time, you initialize CommissionEmployee with a commission value that’s based on the sales for the employee.

With .calculate_payroll(), you leverage the implementation of the base class to retrieve the fixed salary, and you add the commission value.

Since CommissionEmployee derives from SalaryEmployee, you have access to the weekly_salary property directly, and you could’ve implemented .calculate_payroll() using the value of that property.

The problem with accessing the property directly is that if the implementation of SalaryEmployee.calculate_payroll() changes, then you’ll have to also change the implementation of CommissionEmployee.calculate_payroll(). It’s better to rely on the already-implemented method in the base class and extend the functionality as needed.

You’ve created your first class hierarchy for the system. The UML diagram of the classes looks like this:

Inheritance example with multiple Employee derived classes

The diagram shows the inheritance hierarchy of the classes. The derived classes implement the IPayrollCalculator interface, which the PayrollSystem requires. The PayrollSystem.calculate_payroll() implementation requires that the objects in the employees collection contain an .id, .name, and .calculate_payroll() implementation.

Note: Interfaces are represented similarly to classes in UML diagrams, with the word Interface above the interface name. Interface names are usually prefixed with a capital I.

In Python, you don’t implement interfaces explicitly. Instead, interfaces are defined by the attributes used and methods called by other functions and methods.

Next, create a new file and call it program.py. This program creates the employees and passes them to the payroll system to process payroll:

Python program.py
import hr

salary_employee = hr.SalaryEmployee(1, "John Smith", 1500)
hourly_employee = hr.HourlyEmployee(2, "Jane Doe", 40, 15)
commission_employee = hr.CommissionEmployee(3, "Kevin Bacon", 1000, 250)

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(
    [salary_employee, hourly_employee, commission_employee]
)

You can run the program in the command line and see the results:

Shell
$ python program.py

Calculating Payroll
===================
Payroll for: 1 - John Smith
- Check amount: 1500

Payroll for: 2 - Jane Doe
- Check amount: 600

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

The program creates three employee objects, one for each of the derived classes. Then, it creates the payroll system and passes a list of the employees to its .calculate_payroll() method, which calculates the payroll for each employee and prints the results.

Notice how the Employee base class doesn’t define a .calculate_payroll() method. This means that if you were to create a plain Employee object and pass it to the PayrollSystem, then you’d get an error. You can try it in the Python interactive interpreter:

Python
>>> import hr
>>> employee = hr.Employee(1, "Invalid")
>>> payroll_system = hr.PayrollSystem()
>>> payroll_system.calculate_payroll([employee])

Calculating Payroll
===================
Payroll for: 1 - Invalid
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/martin/hr.py", line 7, in calculate_payroll
    print(f"- Check amount: {employee.calculate_payroll()}")
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Employee' object has no attribute 'calculate_payroll'

While you can instantiate an Employee object, PayrollSystem can’t use the object. Why? Because it can’t call .calculate_payroll() for Employee. To be more explicit about the requirements of PayrollSystem, you can convert the Employee class, which is currently a concrete class, to an abstract class. That way, no employee is ever just an Employee, but instead always a derived class that implements .calculate_payroll().

Abstract Base Classes in Python

The Employee class in the example above is what is called an abstract base class. Abstract base classes exist to be inherited, but never instantiated. Python provides the abc module to formally define abstract base classes.

You can use leading underscores in your class name to communicate that objects of that class shouldn’t be created. Underscores provide a friendly way to prevent misuse of your code, but they don’t prevent eager users from creating instances of that class.

The abc module in the Python standard library provides functionality to prevent creating objects from abstract base classes.

You can modify the implementation of the Employee class to ensure that it can’t be instantiated:

Python hr.py
from abc import ABC, abstractmethod

# ...

class Employee(ABC):
    def __init__(self, id, name):
        self.id = id
        self.name = name

    @abstractmethod
    def calculate_payroll(self):
        pass

You derive Employee from ABC, making it an abstract base class. Then, you decorate the .calculate_payroll() method with the @abstractmethod decorator.

This change has two nice side-effects:

  1. You’re telling users of the module that objects of type Employee can’t be created.
  2. You’re telling other developers working on the hr module that if they derive from Employee, then they must override the .calculate_payroll() abstract method.

You can see that you can’t create objects of type Employee anymore using the interactive interpreter:

Python
>>> import hr
>>> employee = hr.Employee(1, "Abstract")
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class Employee
⮑ with abstract method calculate_payroll

The output shows that you can’t instantiate the class because it contains an abstract method, .calculate_payroll(). Derived classes must override the method to allow creating objects of their type.

Implementation Inheritance vs Interface Inheritance

When you derive one class from another, the derived class inherits both of the following:

  1. The base class interface: The derived class inherits all the methods, properties, and attributes of the base class.

  2. The base class implementation: The derived class inherits the code that implements the class interface.

Most of the time, you’ll want to inherit the implementation of a class, but you’ll want to implement multiple interfaces so that you can use your objects in different situations.

Modern programming languages are designed with this basic concept in mind. They allow you to inherit from a single class, but you can implement multiple interfaces.

In Python, you don’t have to explicitly declare an interface. Any object that implements the desired interface can be used in place of another object. This is known as duck typing. Duck typing is usually explained as if it walks like a duck and it quacks like a duck, then it must be a duck. In other words, it’s enough to behave like a duck to be considered a duck.

To illustrate this, you’ll now add a DisgruntledEmployee class to the example above, and it won’t derive from Employee. Create a new file called disgruntled.py and add the following code:

Python disgruntled.py
class DisgruntledEmployee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def calculate_payroll(self):
        return 1_000_000

The DisgruntledEmployee class doesn’t derive from Employee, but it exposes the same interface that PayrollSystem requires. Remember that PayrollSystem.calculate_payroll() requires a list of objects that implement the following interface:

  • An .id property or attribute that returns the employee’s ID
  • A .name property or attribute that represents the employee’s name
  • A .calculate_payroll() method that doesn’t take any parameters and returns the payroll amount to process

The DisgruntledEmployee class meets all these requirements, so PayrollSystem can still calculate its payroll.

You can modify the program to use the DisgruntledEmployee class:

Python program.py
import hr
import disgruntled

salary_employee = hr.SalaryEmployee(1, "John Smith", 1500)
hourly_employee = hr.HourlyEmployee(2, "Jane Doe", 40, 15)
commission_employee = hr.CommissionEmployee(3, "Kevin Bacon", 1000, 250)
disgruntled_employee = disgruntled.DisgruntledEmployee(20000, "Anonymous")

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(
    [
        salary_employee,
        hourly_employee,
        commission_employee,
        disgruntled_employee,
    ]
)

The program creates a DisgruntledEmployee object and adds it to the list that PayrollSystem processes. You can now run the program and see its output:

Shell
$ python program.py

Calculating Payroll
===================
Payroll for: 1 - John Smith
- Check amount: 1500

Payroll for: 2 - Jane Doe
- Check amount: 600

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

Payroll for: 20000 - Anonymous
- Check amount: 1000000

As you can see, the PayrollSystem can still process the new object because it meets the desired interface.

Since you don’t have to derive from a specific class for your objects to be reusable by the program, you may be asking why you should use inheritance instead of just implementing the desired interface. The following rules may help you to make this decision:

  • Use inheritance to reuse an implementation: Your derived classes should leverage most of their base class implementation. They must also model an is a relationship. A Customer class might also have an .id and a .name, but a Customer is not an Employee, so in this case, you shouldn’t use inheritance.

  • Implement an interface to be reused: When you want your class to be reused by a specific part of your application, you implement the required interface in your class, but you don’t need to provide a base class, or inherit from another class.

You can now clean up the example above to move on to the next topic. You can delete the disgruntled.py file and then modify the hr module to its original state:

Python hr.py
class PayrollSystem:
    def calculate_payroll(self, employees):
        print("Calculating Payroll")
        print("===================")
        for employee in employees:
            print(f"Payroll for: {employee.id} - {employee.name}")
            print(f"- Check amount: {employee.calculate_payroll()}")
            print("")

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

class SalaryEmployee(Employee):
    def __init__(self, id, name, weekly_salary):
        super().__init__(id, name)
        self.weekly_salary = weekly_salary

    def calculate_payroll(self):
        return self.weekly_salary

class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

class CommissionEmployee(SalaryEmployee):
    def __init__(self, id, name, weekly_salary, commission):
        super().__init__(id, name, weekly_salary)
        self.commission = commission

    def calculate_payroll(self):
        fixed = super().calculate_payroll()
        return fixed + self.commission

You removed the import of the abc module since the Employee class doesn’t need to be abstract. You also removed the abstract .calculate_payroll() method from it since it doesn’t provide any implementation.

Basically, you’re inheriting the implementation of the .id and .name attributes of the Employee class in your derived classes. Since .calculate_payroll() is just an interface to the PayrollSystem.calculate_payroll() method, you don’t need to implement it in the Employee base class.

Notice how the CommissionEmployee class derives from SalaryEmployee. This means that CommissionEmployee inherits the implementation and interface of SalaryEmployee. You can see how the CommissionEmployee.calculate_payroll() method leverages the base class implementation because it relies on the result from super().calculate_payroll() to implement its own version.

The Class Explosion Problem

If you’re not careful, inheritance can lead you to a huge hierarchical class structure that’s hard to understand and maintain. This is known as the class explosion problem.

You started building a class hierarchy of Employee types used by the PayrollSystem to calculate payroll. Now, you need to add some functionality to those classes so that you can use them with the new ProductivitySystem.

ProductivitySystem tracks productivity based on employee roles. There are different employee roles:

  • Managers: They walk around yelling at people, telling them what to do. They’re salaried employees and make more money.
  • Secretaries: They do all the paperwork for managers and ensure that everything gets billed and payed on time. They’re also salaried employees but make less money.
  • Sales employees: They make a lot of phone calls to sell products. They have a salary, but they also get commissions for sales.
  • Factory workers: They manufacture the products for the company. They’re paid by the hour.

With those requirements, you start to see that Employee and its derived classes might belong somewhere other than the hr module because now they’re also used by the ProductivitySystem.

You create an employees module and move the classes there:

Python employees.py
class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

class SalaryEmployee(Employee):
    def __init__(self, id, name, weekly_salary):
        super().__init__(id, name)
        self.weekly_salary = weekly_salary

    def calculate_payroll(self):
        return self.weekly_salary

class HourlyEmployee(Employee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

class CommissionEmployee(SalaryEmployee):
    def __init__(self, id, name, weekly_salary, commission):
        super().__init__(id, name, weekly_salary)
        self.commission = commission

    def calculate_payroll(self):
        fixed = super().calculate_payroll()
        return fixed + self.commission

The implementation remains the same, but you move the classes to the employees module. Your hr module is now much smaller and focused on the payroll system:

Python hr.py
class PayrollSystem:
    def calculate_payroll(self, employees):
        print("Calculating Payroll")
        print("===================")
        for employee in employees:
            print(f"Payroll for: {employee.id} - {employee.name}")
            print(f"- Check amount: {employee.calculate_payroll()}")
            print("")

With both hr.py and employees.py in place, you can now update your program to support the change:

Python program.py
import hr
import employees

salary_employee = employees.SalaryEmployee(1, "John Smith", 1500)
hourly_employee = employees.HourlyEmployee(2, "Jane Doe", 40, 15)
commission_employee = employees.CommissionEmployee(3, "Kevin Bacon", 1000, 250)

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(
    [salary_employee, hourly_employee, commission_employee]
)

You run the program and verify that it still works:

Shell
$ python program.py

Calculating Payroll
===================
Payroll for: 1 - John Smith
- Check amount: 1500

Payroll for: 2 - Jane Doe
- Check amount: 600

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

With everything in place, you start adding the new classes:

Python employees.py
# ...

class Manager(SalaryEmployee):
    def work(self, hours):
        print(f"{self.name} screams and yells for {hours} hours.")

class Secretary(SalaryEmployee):
    def work(self, hours):
        print(f"{self.name} expends {hours} hours doing office paperwork.")

class SalesPerson(CommissionEmployee):
    def work(self, hours):
        print(f"{self.name} expends {hours} hours on the phone.")

class FactoryWorker(HourlyEmployee):
    def work(self, hours):
        print(f"{self.name} manufactures gadgets for {hours} hours.")

First, you add a Manager class that derives from SalaryEmployee. The class exposes a .work() method that the productivity system will use. The method takes the hours that the employee worked.

Then you add Secretary, SalesPerson, and FactoryWorker and then implement the .work() interface, so they can be used by the productivity system—which you haven’t created yet.

As a next step, you can create a new file called productivity.py and add the ProductivitySytem class:

Python productivity.py
class ProductivitySystem:
    def track(self, employees, hours):
        print("Tracking Employee Productivity")
        print("==============================")
        for employee in employees:
            employee.work(hours)
        print("")

The class tracks employees in the .track() method that takes a list of employees and the number of hours to track. As outlined above, the productivity system makes use of .work() on each of the objects in employees to accomplish the tracking.

You can now add the productivity system to your program, and update it to represent different types of employees:

Python program.py
import hr
import employees
import productivity

manager = employees.Manager(1, "Mary Poppins", 3000)
secretary = employees.Secretary(2, "John Smith", 1500)
sales_guy = employees.SalesPerson(3, "Kevin Bacon", 1000, 250)
factory_worker = employees.FactoryWorker(4, "Jane Doe", 40, 15)
employees = [
    manager,
    secretary,
    sales_guy,
    factory_worker,
]

productivity_system = productivity.ProductivitySystem()
productivity_system.track(employees, 40)

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(employees)

Your updated program creates a list of employees of different types. The employee list is sent to the productivity system to track their work for forty hours. Then the same list of employees is sent to the payroll system to calculate their payroll.

You can run the program to see the output:

Shell
$ python program.py

Tracking Employee Productivity
==============================
Mary Poppins screams and yells for 40 hours.
John Smith expends 40 hours doing office paperwork.
Kevin Bacon expends 40 hours on the phone.
Jane Doe manufactures gadgets for 40 hours.

Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000

Payroll for: 2 - John Smith
- Check amount: 1500

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

Payroll for: 4 - Jane Doe
- Check amount: 600

The program shows the employees working for forty hours through the productivity system. Then it calculates and displays the payroll for each of the employees.

The program works as expected, but you had to add four new classes to support the changes. As new requirements come, your class hierarchy will inevitably grow, leading to the class explosion problem where your hierarchies will become so big that they’ll be hard to understand and maintain.

The following diagram shows the new class hierarchy:

Class design explosion by inheritance

The diagram shows how the class hierarchy is growing. Additional requirements might have an exponential effect on the number of classes with this design.

Inheriting Multiple Classes

Python is one of the few modern programming languages that supports multiple inheritance. Multiple inheritance is the ability to derive a class from multiple base classes at the same time.

Multiple inheritance has a bad reputation to the extent that most modern programming languages don’t support it. Instead, modern programming languages support the concept of interfaces. In those languages, you inherit from a single base class and then implement multiple interfaces, so you can reuse your classes in different situations.

This approach puts some constraints in your designs. You can only inherit the implementation of one class by directly deriving from it. You can implement multiple interfaces, but you can’t inherit the implementation of multiple classes.

This constraint is good for software design because it forces you to design your classes with fewer dependencies on each other. You will see later in this tutorial that you can leverage multiple implementations through composition, which makes software more flexible. This section, however, is about multiple inheritance, so take a look at how it works.

It turns out that sometimes temporary secretaries are hired when there’s too much paperwork to do. The TemporarySecretary class performs the role of a Secretary in the context of the ProductivitySystem, but for payroll purposes, it’s an HourlyEmployee.

You look at your class design. It’s grown a little bit, but you can still understand how it works. It seems you have two options:

  1. Derive from Secretary: You can derive from Secretary to inherit the .work() method for the role, and then override the .calculate_payroll() method to implement it as an HourlyEmployee.

  2. Derive from HourlyEmployee: You can derive from HourlyEmployee to inherit the .calculate_payroll() method, and then override the .work() method to implement it as a Secretary.

Then, you remember that Python supports multiple inheritance, so you decide to derive from both Secretary and HourlyEmployee:

Python employees.py
# ...

class TemporarySecretary(Secretary, HourlyEmployee):
    pass

Python allows you to inherit from two different classes by specifying them between parentheses in the class declaration, and separating them with commas.

Now, you modify your program to add the new temporary secretary employee:

Python program.py
import hr
import employees
import productivity

manager = employees.Manager(1, "Mary Poppins", 3000)
secretary = employees.Secretary(2, "John Smith", 1500)
sales_guy = employees.SalesPerson(3, "Kevin Bacon", 1000, 250)
factory_worker = employees.FactoryWorker(4, "Jane Doe", 40, 15)
temporary_secretary = employees.TemporarySecretary(5, "Robin Williams", 40, 9)
company_employees = [
    manager,
    secretary,
    sales_guy,
    factory_worker,
    temporary_secretary,
]

productivity_system = productivity.ProductivitySystem()
productivity_system.track(company_employees, 40)

payroll_system = hr.PayrollSystem()
payroll_system.calculate_payroll(company_employees)

You run the program to test it:

Shell
$ python program.py

Traceback (most recent call last):
  File "/Users/martin/program.py", line 9, in <module>
    temporary_secretary = employees.TemporarySecretary(5, "Robin Williams", 40, 9)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: SalaryEmployee.__init__() takes 4 positional arguments but 5 were given

You get a TypeError exception saying that 4 positional arguments where expected, but 5 were given.

This is because you derived TemporarySecretary first from Secretary and then from HourlyEmployee, so the interpreter is trying to use Secretary.__init__() to initialize the object.

Okay, go ahead and reverse it:

Python
# ...

class TemporarySecretary(HourlyEmployee, Secretary):
    pass

Now, run the program again and see what happens:

Shell
$ python program.py

Traceback (most recent call last):
  File "/Users/martin/program.py", line 9, in <module>
    temporary_secretary = employees.TemporarySecretary(5, "Robin Williams", 40, 9)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/martin/employees.py", line 18, in __init__
    super().__init__(id, name)
TypeError: SalaryEmployee.__init__() missing 1 required positional argument: 'weekly_salary'

Now it seems that you’re missing a weekly_salary parameter, which is necessary to initialize Secretary, but that parameter doesn’t make sense in the context of a TemporarySecretary because it’s an HourlyEmployee.

Maybe implementing TemporarySecretary.__init__() will help:

Python employees.py
# ...

class TemporarySecretary(HourlyEmployee, Secretary):
    def __init__(self, id, name, hours_worked, hourly_rate):
        super().__init__(id, name, hours_worked, hourly_rate)

Try it:

Shell
$ python program.py

Traceback (most recent call last):
  File "/Users/martin/program.py", line 9, in <module>
    temporary_secretary = employees.TemporarySecretary(5, "Robin Williams", 40, 9)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/martin/employees.py", line 58, in __init__
    super().__init__(id, name, hours_worked, hourly_rate)
  File "/Users/martin/employees.py", line 18, in __init__
    super().__init__(id, name)
TypeError: SalaryEmployee.__init__() missing 1 required positional argument: 'weekly_salary'

That didn’t work either. Okay, it’s time for you to dive into Python’s method resolution order (MRO) to see what’s going on.

When a method or attribute of a class is accessed, Python uses the class MRO to find it. The MRO is also used by super() to determine which method or attribute to invoke. You can learn more about super() in Supercharge Your Classes With Python super().

You can evaluate the TemporarySecretary class MRO using the interactive interpreter:

Python
>>> from employees import TemporarySecretary
>>> TemporarySecretary.__mro__
(<class 'employees.TemporarySecretary'>,
 <class 'employees.HourlyEmployee'>,
 <class 'employees.Secretary'>,
 <class 'employees.SalaryEmployee'>,
 <class 'employees.Employee'>,
 <class 'object'>)

The MRO shows the order in which Python is going to look for a matching attribute or method. In the example, this is what happens when you create the TemporarySecretary object:

  1. The TemporarySecretary.__init__(self, id, name, hours_worked, hourly_rate) method is called.

  2. The super().__init__(id, name, hours_worked, hourly_rate) call matches HourlyEmployee.__init__(self, id, name, hours_worked, hourly_rate).

  3. HourlyEmployee calls super().__init__(id, name), which the MRO is going to match to Secretary.__init__(), which is inherited from SalaryEmployee.__init__(self, id, name, weekly_salary).

Because the parameters don’t match, Python raises a TypeError exception.

You can bypass parts of the MRO. In this case, you want to skip the initialization of Secretary and SalaryEmployee. You can do this by reversing the inheritance order again back to how you had it initially. Then, you’ll directly call HourlyEmployee.__init__():

Python employees.py
# ...

class TemporarySecretary(Secretary, HourlyEmployee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        HourlyEmployee.__init__(self, id, name, hours_worked, hourly_rate)

When you put Secretary before HourlyEmployee, then the MRO of TemporarySecretary looks like the following:

Python
>>> from employees import TemporarySecretary
>>> TemporarySecretary.__mro__
(<class 'employees.TemporarySecretary'>,
 <class 'employees.Secretary'>,
 <class 'employees.SalaryEmployee'>,
 <class 'employees.HourlyEmployee'>,
 <class 'employees.Employee'>,
 <class 'object'>)

Because you explicitly specified that .__init__() should use HourlyEmployee.__init__(), you’re effectively skipping Secretary and SalaryEmployee in the MRO when initializing an object.

That solves the problem of creating the object, but you’ll run into a similar problem when trying to calculate payroll. You can run the program to see the problem:

Shell
$ python program.py

Tracking Employee Productivity
==============================
Mary Poppins screams and yells for 40 hours.
John Smith expends 40 hours doing office paperwork.
Kevin Bacon expends 40 hours on the phone.
Jane Doe manufactures gadgets for 40 hours.
Robin Williams expends 40 hours doing office paperwork.

Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000

Payroll for: 2 - John Smith
- Check amount: 1500

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

Payroll for: 4 - Jane Doe
- Check amount: 600

Payroll for: 5 - Robin Williams
Traceback (most recent call last):
  File "/Users/martin/program.py", line 22, in <module>
    payroll_system.calculate_payroll(company_employees)
  File "/Users/martin/hr.py", line 7, in calculate_payroll
    print(f"- Check amount: {employee.calculate_payroll()}")
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/martin/employees.py", line 13, in calculate_payroll
    return self.weekly_salary
           ^^^^^^^^^^^^^^^^^^
AttributeError: 'TemporarySecretary' object has no attribute 'weekly_salary'

The problem now is that because you reversed the inheritance order, the MRO is finding the .calculate_payroll() method of SalariedEmployee before the one in HourlyEmployee. You need to override .calculate_payroll() in TemporarySecretary and invoke the right implementation from it:

Python
# ...

class TemporarySecretary(Secretary, HourlyEmployee):
    def __init__(self, id, name, hours_worked, hourly_rate):
        HourlyEmployee.__init__(self, id, name, hours_worked, hourly_rate)

    def calculate_payroll(self):
        return HourlyEmployee.calculate_payroll(self)

The new .calculate_payroll() method now directly invokes HourlyEmployee.calculate_payroll() to ensure that you get the correct result. You can run the program again to see it working:

Shell
$ python program.py

Tracking Employee Productivity
==============================
Mary Poppins screams and yells for 40 hours.
John Smith expends 40 hours doing office paperwork.
Kevin Bacon expends 40 hours on the phone.
Jane Doe manufactures gadgets for 40 hours.
Robin Williams expends 40 hours doing office paperwork.

Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000

Payroll for: 2 - John Smith
- Check amount: 1500

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

Payroll for: 4 - Jane Doe
- Check amount: 600

Payroll for: 5 - Robin Williams
- Check amount: 360

The program now works as expected because you’re forcing the method resolution order by explicitly telling the interpreter which method you want to use.

As you can see, multiple inheritance can be confusing, especially when you run into the diamond problem.

The following diagram shows the diamond problem in your class hierarchy:

Diamond problem caused by multiple inheritance

The diagram shows the diamond problem with the current class design. TemporarySecretary uses multiple inheritance to derive from two classes that ultimately also derive from Employee. This causes two paths to reach the Employee base class, which is something you want to avoid in your designs.

The diamond problem appears when you’re using multiple inheritance and deriving from two classes that have a common base class. This can cause the wrong version of a method to be called.

As you’ve seen, Python provides a way to force the right method to be invoked, and analyzing the MRO can help you understand the problem.

Still, when you run into the diamond problem, it’s better to rethink the design. You’ll now make some changes to leverage multiple inheritance, avoiding the diamond problem.

Two different systems use the Employee derived classes:

  1. The productivity system that tracks employee productivity

  2. The payroll system that calculates the employee payroll

This means that everything related to productivity should be together in one module, and everything related to payroll should be together in another. You can start making changes to the productivity module:

Python productivity.py
class ProductivitySystem:
    def track(self, employees, hours):
        print("Tracking Employee Productivity")
        print("==============================")
        for employee in employees:
            result = employee.work(hours)
            print(f"{employee.name}: {result}")
        print("")

class ManagerRole:
    def work(self, hours):
        return f"screams and yells for {hours} hours."

class SecretaryRole:
    def work(self, hours):
        return f"expends {hours} hours doing office paperwork."

class SalesRole:
    def work(self, hours):
        return f"expends {hours} hours on the phone."

class FactoryRole:
    def work(self, hours):
        return f"manufactures gadgets for {hours} hours."

The productivity module implements the ProductivitySystem class, as well as the related roles that it supports. The classes implement the .work() interface required by the system, but they don’t derive from Employee.

You can do the same with the hr module:

Python hr.py
class PayrollSystem:
    def calculate_payroll(self, employees):
        print("Calculating Payroll")
        print("===================")
        for employee in employees:
            print(f"Payroll for: {employee.id} - {employee.name}")
            print(f"- Check amount: {employee.calculate_payroll()}")
            print("")

class SalaryPolicy:
    def __init__(self, weekly_salary):
        self.weekly_salary = weekly_salary

    def calculate_payroll(self):
        return self.weekly_salary

class HourlyPolicy:
    def __init__(self, hours_worked, hourly_rate):
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate

    def calculate_payroll(self):
        return self.hours_worked * self.hourly_rate

class CommissionPolicy(SalaryPolicy):
    def __init__(self, weekly_salary, commission):
        super().__init__(weekly_salary)
        self.commission = commission

    def calculate_payroll(self):
        fixed = super().calculate_payroll()
        return fixed + self.commission

The hr module implements the PayrollSystem, which calculates payroll for the employees. It also implements the policy classes for payroll. As you can see, the policy classes don’t derive from Employee anymore.

You can now add the necessary classes to the employee module:

Python employees.py
from hr import SalaryPolicy, CommissionPolicy, HourlyPolicy
from productivity import ManagerRole, SecretaryRole, SalesRole, FactoryRole

class Employee:
    def __init__(self, id, name):
        self.id = id
        self.name = name

class Manager(Employee, ManagerRole, SalaryPolicy):
    def __init__(self, id, name, weekly_salary):
        SalaryPolicy.__init__(self, weekly_salary)
        super().__init__(id, name)

class Secretary(Employee, SecretaryRole, SalaryPolicy):
    def __init__(self, id, name, weekly_salary):
        SalaryPolicy.__init__(self, weekly_salary)
        super().__init__(id, name)

class SalesPerson(Employee, SalesRole, CommissionPolicy):
    def __init__(self, id, name, weekly_salary, commission):
        CommissionPolicy.__init__(self, weekly_salary, commission)
        super().__init__(id, name)

class FactoryWorker(Employee, FactoryRole, HourlyPolicy):
    def __init__(self, id, name, hours_worked, hourly_rate):
        HourlyPolicy.__init__(self, hours_worked, hourly_rate)
        super().__init__(id, name)

class TemporarySecretary(Employee, SecretaryRole, HourlyPolicy):
    def __init__(self, id, name, hours_worked, hourly_rate):
        HourlyPolicy.__init__(self, hours_worked, hourly_rate)
        super().__init__(id, name)

The employees module imports policies and roles from the other modules and implements the different Employee types. You’re still using multiple inheritance to inherit the implementation of the salary policy classes and the productivity roles, but the implementation of each class only needs to deal with initialization.

Notice that you still need to explicitly initialize the salary policies in the constructors. You probably saw that the initializations of Manager and Secretary are identical. Also, the initializations of FactoryWorker and TemporarySecretary are the same.

You won’t want to have this kind of code duplication in more complex designs, so you have to be careful when designing class hierarchies.

Here’s the UML diagram for the new design:

Policy based design using multiple inheritance

The diagram shows the relationships to define the Secretary and TemporarySecretary using multiple inheritance, but avoiding the diamond problem.

You can run the program and see how it works:

Shell
$ python program.py

Tracking Employee Productivity
==============================
Mary Poppins: screams and yells for 40 hours.
John Smith: expends 40 hours doing office paperwork.
Kevin Bacon: expends 40 hours on the phone.
Jane Doe: manufactures gadgets for 40 hours.
Robin Williams: expends 40 hours doing office paperwork.

Calculating Payroll
===================
Payroll for: 1 - Mary Poppins
- Check amount: 3000

Payroll for: 2 - John Smith
- Check amount: 1500

Payroll for: 3 - Kevin Bacon
- Check amount: 1250

Payroll for: 4 - Jane Doe
- Check amount: 600

Payroll for: 5 - Robin Williams
- Check amount: 360

You’ve seen how inheritance and multiple inheritance work in Python. You can now explore the topic of composition.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK