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.
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?
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?
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!
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.
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
.
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
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.
lounap on Oct. 3, 2023
Thank you! It is starting to sink in. :-)
Become a Member to join the conversation.
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.