Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Avoiding the Diamond Problem

00:00 Our project is still fairly small, so it won’t be a big hassle to redesign it to avoid the diamond problem. Here’s how this is going to work. We have four modules: program, which is the actual script we run, as well as hr, productivity, and employees.

00:21 hr is going to contain all the policies that are used for the PayrollSystem, like hourly, salary, or commission. productivity will define roles that determine how employees are tracked in the productivity system, like manager, secretary, or salesperson.

00:42 The employees module will define a class for each type of employee, leveraging multiple inheritance. Each class will inherit a payroll policy to be used in the PayrollSystem and a role to be used in the ProductivitySystem.

01:00 What’s cool about this is that our role and policy classes will not explicitly inherit from anything else. This means that when we instantiate our employee classes, each of which relies on multiple inheritance, we won’t have to go digging through the MRO to figure out what’s going on under the hood. I think we can all agree that’s a good thing.

01:24 This UML diagram on the right represents a portion of this redesign. Specifically, this shows the design of the Secretary and TemporarySecretary classes.

01:37 It looks like it’s more complicated at first, but if you look at it carefully, you can see that it avoids the diamond problem.

01:47 We have our base Employee class and a Secretary and a TemporarySecretary that inherit from it. The Secretary also inherits a SalaryPolicy that defines how its payroll is calculated and a SecretaryRole that defines how its hours are tracked in the ProductivitySystem.

02:10 You can see the TemporarySecretary also inherits the SecretaryRole, but instead of inheriting the SalaryPolicy, it inherits the HourlyPolicy.

02:22 The interfaces show us that the role class conforms to the ProductivitySystem and the policy classes conform to the PayrollSystem.

02:33 What we’ve done here is utilize multiple inheritance to decouple these various classes from the various systems they must conform to. All the code relating to one system will live in a single module, which also makes things easy to find.

02:51 Let’s see how this looks in code. I am here in Visual Studio Code in program.py, and one thing I want to point out here is that this is basically going to be a drop-in replacement of our other modules.

03:07 What I mean by that is that after we modify the other modules, we won’t have to change the main program module at all. Everything will still just work because we’re not actually changing the underlying interfaces.

03:24 I’m going to start work in the productivity module, which currently just contains the ProductivitySystem. What we want to do is define a policy for each type of employee that defines how they actually work. Let’s start with the manager.

03:43 This class will be called ManagerRole, and it won’t inherit from anything. It will just define a single method .work() that will look similar to before.

03:56 And now, through the magic of editing, I will define the other three role classes that work in the exact same way.

04:06 Notice that instead of having the .work() method print something to the screen directly, we’re just returning a string. That’s fine, but that means that we have to modify the ProductivitySystem to account for this change. Fortunately for us, that’s right up here at the top.

04:28 I’ll delete the body of this for loop and then store the return value of calling each employee’s .work() method in this variable called result.

04:41 And now I’ll simply print the f-string f"{employee.name}: {result}", and that’s it for the ProductivitySystem. Next, let’s attack the HR system.

04:55 This time, we won’t modify the PayrollSystem at all but we will add some new classes that define policies it will use. I’ll start with the SalaryPolicy, which will be inherited by an employee with a salary.

05:13 Just like with the ProductivitySystem, this will not inherit from any other classes. The .__init__() method will simply initialize an instance attribute for the weekly salary. For .calculate_payroll(), we will simply return this instance attribute.

05:33 I’ve gone ahead and created the other two policy classes. This is all familiar code, just in a new place. Remember that CommissionPolicy inherits from SalaryEmployee because someone who is paid a commission also receives a weekly salary. As such, we use the super() function to get the salary amount and add it to the commissions amount in the .calculate_payroll() method.

06:03 The last module we need to work on is employees.py. This is going to require some big changes, so I think it’s easiest if we just nuke it and start fresh.

06:16 The first thing I want to do is import the new classes we created from our other modules. We should be able to get away with using import * here, but for good practice, I’ll list the classes we want to import manually.

06:33 I’ll say from hr import (SalaryPolicy, CommissionPolicy, HourlyPolicy), from productivity import (ManagerRole, SecretaryRole, SalesRole, FactoryRole).

06:55 Now, I’m going to recreate the Employee class.

07:01 This will look the exact same as it did before.

07:06 Next, we are going to create the specialized employee classes. Each one of these classes will inherit first from Employee, then from a productivity role class, then from a payroll policy class.

07:23 Let’s start with the Manager. The manager is an employee who gets tracked as a manager and paid a salary. They aren’t paid hourly, so it doesn’t make sense to give them hours_worked or hour_rate arguments.

07:43 We just want to make sure the Manager has a .weekly_salary attribute and a .calculate_payroll() method, so I’m going to call the SalaryPolicy class’s .__init__() method.

07:57 As for the .work() method, that will automatically be inherited from the ManagerRole class, and we don’t have to initialize anything because that .work() method doesn’t rely on any instance attributes—just an hours argument.

08:14 All that’s left to do is initialize the Employee base class, which we can do with super(). super() will follow the MRO, but the first place it will look is Employee since we inherited from that class first.

08:32 The signature for this .__init__() method matches that of the .__init__() method in Employee,

08:39 and that method doesn’t call super(), so it will stop there. Our Manager class is now created.

08:48 The Secretary class looks the exact same, except they inherit from SecretaryRole instead of ManagerRole. These last three classes are also very similar.

09:01 You might notice a pattern here within the .__init__() method. Initialize the payroll policy to obtain the payroll instance attributes needed by our inherited .calculate_payroll() method, then call super() to initialize the Employee base class, giving this class a name and an id.

09:24 Now that the redesign is complete, I’m going to move back over to program.py, and if all is working, we should be able to run this without modifying any of these object instantiations. That’s because we redesigned these classes but we kept their interfaces the same.

09:46 The .__init__() methods for each employee type still requires the same list of arguments as before. I will run this, and we see that everything still works.

09:59 It might seem like we did a whole lot of work for nothing, but by redesigning the software, we’ve now avoided the diamond problem and we’ve ensured that it’ll be easy to create new employee types, roles, and payroll policies in the future, without having to dig through the MRO to figure out what kind of new classes we are creating. In the next video, you’ll see how we can extend this program’s functionality with composition.

Avatar image for dwalsh

dwalsh on May 27, 2020

Really great video. Once I got through the Multiple Inheritance video about 5 or 6 times to understand the intricacies, this made way more sense and is so much cleaner in terms of inheritance.

Avatar image for saljon

saljon on March 19, 2021

Why we call weekly_salary by using the class name SalaryPolicy why we didn’t use super() as we did with id and name and what’s the differencie between the two ways?

Avatar image for saljon

saljon on March 19, 2021

I am having issue in my code:

Traceback (most recent call last):
  File "C:/Users/Sada/PycharmProjects1/ProjectRealPython/program.py", line 9, in <module>
    temporary_secretary = employees.SecretaryTemporary(5, 'Robin Williams', 40, 9)
TypeError: __init__() takes 3 positional arguments but 5 were given

And this is my code:

class SecretaryTemporary(Employee, SecretaryRold, HourlyPolicy):
    def __init(self, id, name, hours_worked, hour_rate):
        HourlyPolicy.__init__(self, hours_worked, hour_rate)
        super().__init__(id, name)

Why do I have this kind of exception even though in my code I give the same exact number of arguments? Any help?

Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on March 22, 2021

@saljon This example uses a mix of the old and the new style of doing inheritance in Python. In the old style, which was common in Python 2.x, you had to indicate the base class by name to initialize it, whereas in Python 3.x, you can also leverage the super() function.

However, things get a little more complicated with multiple inheritance, i.e., when your class extends more than one base class. The super() function will figure out the order of calling the base classes’ initializers, and it’ll pass all its arguments to each. Therefore, if you choose to use super() and need to pass parameters through it, then you also have to make sure that all .__init__() methods in your base classes can accept arbitrary number of parameters. The usual way to do this is with *args and **kwargs:

class Employee:
    def __init__(self, id, name, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.id = id
        self.name = name

class ManagerRole:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

class SalaryPolicy:
    def __init__(self, weekly_rate, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.weekly_rate = weekly_rate

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

Notice how every class makes a call to super() and declares *args and **kwargs even when it doesn’t accept any parameters in its initializer!

Avatar image for iamrsingh

iamrsingh on Feb. 27, 2022

One question, regarding using the payment policies as a standalone class and adding them as super class into employees. Can these be called as mixins? As they don’t inherit from any super class and are pluggable into any type of employee. Am I correct or am I misunderstanding them? Please help.

Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on March 1, 2022

@iamrsingh That’s about right. Mixins aren’t standalone classes, which only become useful as a part of another class. However, mixins are typically stateless, and they don’t define any member attributes such as weekly_rate.

Avatar image for lounap

lounap on Oct. 2, 2023

Hello. I really appreciate the learning content, thank you! I did have one question. Why do we need to pass self when calling .__init__() on a specific base class but not when we call .__init__() with super()?

Thank you, Louis

Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on Oct. 2, 2023

@lounap Excellent question, Louis!

When you call the .__init__() method on a superclass explicitly, like BaseClass.__init__(self, args), you need to pass self because you access that method using a reference that isn’t bound to any specific object. On the other hand, calling super() returns a concrete instance of your class, so effectively, super() becomes analogous to self in that context.

Here’s a little example to demonstrate that:

>>> class BaseClass:
...     pass
...
>>> class SubClass(BaseClass):
...     def __init__(self):
...         unbound_reference = BaseClass.__init__
...         bound_reference = super().__init__
...
...         unbound_reference(self)
...         bound_reference()
...
...         print(unbound_reference)
...         print(bound_reference)
...
>>> SubClass()
<slot wrapper '__init__' of 'object' objects>
<method-wrapper '__init__' of SubClass object at 0x7f575852be50>
<__main__.SubClass object at 0x7f575852be50>

Notice that the second method reference is bound to the particular object whose identity is 0x7f575852be50, whereas the other reference isn’t.

Avatar image for lounap

lounap on Oct. 3, 2023

Thank you! It is starting to sink in. :-)

Become a Member to join the conversation.