Implementing a Class Hierarchy
00:00 Now that you understand the basics of inheritance and composition, it’s time to apply them to a real project. Throughout the rest of this course, you are going to build a simple application that could be used by a human resources department to track employees’ productivity and calculate their payrolls. As you code, you’ll learn about some new object-oriented programming techniques and you’ll get some invaluable practice with inheritance and composition.
00:33
I am here inside of Visual Studio Code, working within a directory called HR
, and I’ve got open a blank Python file called hr.py
.
00:45
The first class we are going to build is called PayrollSystem
. This class will be in charge of calculating the payrolls for all of the employees in this imaginary company.
00:58
The PayrollSystem
will not have any attributes. Instead, it will just declare and implement a method called .calculate_payroll()
, which will accept a collection of employees as a parameter.
01:15
Now, I’ll just print 'Calculating Payroll'
and I’ll insert a divider here to separate the rest of the output.
01:26
I’m going to use a for
loop to iterate through the collection of employees, and for each employee
I’ll print the f-string f'Payroll: for {employee.id} - {employee.name}'
.
01:48
And now, let’s print another f-string that calls the .calculate_payroll()
method of each employee
object to determine their paycheck amount.
02:01
I’ll print a blank line just to keep things separated. Okay, this is a good start, but we haven’t defined what it means to be an employee. According to this class, employees must have attributes for .name
and .id
, and they must implement a .calculate_payroll()
method.
02:23 That’s because this class utilizes all of those members. The rest of this part of the program will follow from this UML diagram that you’re familiar with by now.
02:37
Any class that implements this IPayrollCalculator
is one that can be passed into this PayrollSystem
. That’s because the PayrollSystem
requires the same members listed in the IPayrollCalculator
interface.
02:56
Let’s build each class one by one, starting with the base Employee
class.
03:03
I’ll create a new class here called Employee
. Because this is the base class of everything else, it won’t explicitly inherit from anything, but remember that it’s still inheriting from that object
superclass behind the scenes.
03:21
According to our UML diagram, this Employee
must contain two instance methods, .id
and .name
. I’ll create an .__init__()
method with parameters for id
and name
, and then assign those to the instance attributes.
03:42
Now, let’s create our first specialized employee class. The first class is SalaryEmployee
, which will act just like a regular employee, except they will also have a .weekly_salary
instance attribute that will be used in their .calculate_payroll()
method.
04:02
So first, I will create the class, and we want to make sure we’re inheriting from Employee
. At this point, SalaryEmployee
is functionally identical to Employee
, but that’s pretty boring. Just like before, I’m going to create an .__init__()
method, but this time I will also accept a weekly_salary
as a parameter.
04:29
Now, we’re accepting an id
, a name
and a weekly_salary
, but we haven’t done anything with it yet. The first thing we should do is initialize the parent class’s .__init__()
method.
04:44
This will allow us to access the instance methods declared in the parent from within the child. If we were to omit the .__init__()
method in the child, it would automatically inherit everything from the parent.
04:59
But once we declare this .__init__()
method in the child, it covers up all of the instance attributes in the parent, and we must set them manually by calling the .__init__()
method in the parent.
05:13
We can do that with the super()
function, just like this. Now, any SalaryEmployee
object can also be used as an Employee
object,
05:25
but we still have to deal with this .weekly_salary
attribute. After all, this is a specialized version of Employee
. That will be a new instance attribute.
05:39
In order for this class to conform to IPayrollCalculator
, which is required to be used by the PayrollSystem
class, it must also implement a .calculate_payroll()
method that defines how a SalaryEmployee
is paid. To do that, I’ll create the .calculate_payroll()
method, passing in self
as a parameter.
06:05
And I will simply return the .weekly_salary
instance attribute. This seems kind of trivial now, but later on, you’ll see why it’s important that we returned this instance attribute through a method.
06:21
Every time something asks a SalaryEmployee
to calculate their payroll, they will return their .weekly_salary
value through this method.
06:31
At this point, you might be wondering, “Why do we need to call the .__init__()
method of the parent when we instantiate a child class?” The reason for that is because the child class inherits the attributes of the parent, but it can’t use them unless it initializes them with some value.
06:52
Like I mentioned before, if we didn’t call the parent’s .__init__()
method with the required arguments but we did include our own .__init__()
method in this child class, then our SalaryEmployee
would just have a .weekly_salary
attribute, but no .name
or .id
.
07:09
It wouldn’t really be an Employee
, either, and so it wouldn’t conform to the IPayrollCalculator
interface. Again, if you don’t declare an .__init__()
method in the child class, then it will use its parent’s .__init__()
method by default and call that when you try to instantiate the child class.
07:31
But if you do declare an .__init__()
method in the child class, that will override the .__init__()
method in the parent, so you usually want to call the parent’s .__init__()
method manually, either by name or with the super()
function, like we did here.
07:48
Next, let’s create the HourlyEmployee
, which will also inherit from Employee
. This type of employee is paid by the hour, and so it will also need two additional instance attributes: .hours_worked
and .hour_rate
.
08:07
Just like before, I will call the .__init__()
method of the parent so we satisfy the Employee
requirements, name
and id
.
08:18
Now we can create these two new instance attributes, which are unique to the HourlyEmployee
objects. Finally, we just need to implement the .calculate_payroll()
method.
08:33
Each time the HourlyEmployee
object is told to calculate their payroll, they should return the number of hours they’ve worked times their hourly rate.
08:48
The last specialized class we need to create is the CommissionEmployee
. This class is a little special, though, and that’s because a commission employee receives both a commission and a weekly salary—lucky them! Because they are technically also a salary employee, we want to make them inherit from SalaryEmployee
instead of the base Employee
class. They share so much in common, so we might as well.
09:20
This class will need an id
, a name
, a weekly_salary
, and a commission
.
09:29
I’m going to initialize the SalaryEmployee
class, which we told to also initialize the Employee
class. This way, our CommissionEmployee
will be a SalaryEmployee
and an Employee
.
09:46
The .__init__()
method for the SalaryEmployee
requires an id
, a name
, and a weekly_salary
, so I’ll pass those values in.
09:57
I’m also going to create a new instance attribute for the commission. All that’s left to do now is implement the .calculate_payroll()
method.
10:08
A commission employee’s pay should be the sum of their commissions plus the weekly salary that they receive, but the .weekly_salary
is not an instance attribute of CommissionEmployee
.
10:23
It’s an instance attribute of its parent, SalaryEmployee
. We initialized the .weekly_salary
, so we can use it, but instead of accessing that attribute directly with the super()
function, let’s think about a better way to do this.
10:40
This CommissionEmployee
is a SalaryEmployee
, and so when it calculates the salary part of its pay, it should do so in the same way as a normal SalaryEmployee
.
10:53
That being said, let’s create a new variable called fixed
, which will store the .weekly_salary
value obtained by calling the parent class’s .calculate_payroll()
method. Now, this might seem a little bit strange because right now the parent class’s .calculate_salary()
method just returns the .weekly_salary
attribute. I mean, we could have just accessed that directly. But think about this: what if the way we calculate the SalaryEmployee
salary changes in the future?
11:28
That would mean that we’d change the implementation of its .calculate_payroll()
method in the SalaryEmployee
class, and because a CommissionEmployee
is a SalaryEmployee
, we’d want that change to trickle down to the CommissionEmployee
too.
11:46
By making the CommissionEmployee
obtain its .weekly_salary
component through the SalaryEmployee
class’s .calculate_payroll()
method, we’ve future-proofed this relationship from any changes in the parent.
12:00
Finally, we want to return this fixed
salary amount, plus the current .commission
value.
12:08 Great. Now, we’ve created a program that models this UML diagram I have onscreen.
12:15
All that’s left to do is test it by creating some objects. To do that, I’m first going to create a new file called program.py
. This will be the entry point of our HR program, and so this is the script we should always run.
12:34
The first thing we want to do is bring the other classes into this namespace, which we can do by simply importing hr
. Make sure hr.py
and program.py
are in the same directory, or folder,
12:51
or you might run into some issues. The first kind of employee we’re going to create is the SalaryEmployee
. We’ll get that object with hr.SalaryEmployee
passing in an id
of 1
, a name
of 'John Smith'
, and a weekly_salary
of 1500
dollars.
13:16
I’ll create the HourlyEmployee
object the same way, except this employee will work 40
hours a week at a rate of 15
dollars per hour. For the CommissionEmployee
, I want them to get a base salary of 1000
dollars a week, plus 250
dollars in commissions.
13:38
Now, all that’s left to do is to create the PayrollSystem
and pass in these Employee
objects to its .calculate_payroll()
method.
13:48
We need to make sure we pass in these employees in the form of a list, so the PayrollSystem
’s .calculate_payroll()
method can iterate through them and calculate their payrolls individually.
14:02 I will run this program and we get exactly the output we were expecting.
Austin Cepalia RP Team on April 17, 2020
@Zarata it should! weekly_salary is an instance attribute of SalaryEmployee, so it should be on the UML diagram in that class. That looks to be a small error in the original article, which unfortunately trickled down to my course.
Percy Boomer on April 17, 2020
@Zarata,it would seem we have scope creep for this project. I think you’re right.
KoJans on May 11, 2020
If I run the code for hr.py as in ‘Implementing a Class Hierarchy’ on my system I get an error:
TypeError: calculate_payroll() missing 1 required positional argument: 'employees'
If I leave out the ‘self’ argument in calculate_payroll in the class PayrollSystem:
def calculate_payroll(employees)
the code works as expected. So I think the ‘self’ argument should not be in the method?
Roy Telles on Aug. 12, 2020
@KoJans I just received the same error. I fixed it, but the implementation that gave me the error was this:
class CommissionEmployee(SalaryEmployee):
def __init__(self, id, name, weekly_salary, commission):
super().__init__(self, id, name, weekly_salary)
self.commission = commission
def calculate_payroll(self):
fixed = super().calculate_payroll()
return fixed + self.commission
Notice how the CommissionEmployee
class passes self
into the super().__init__
call. The error is generated because if you recall, that with super()
all we need to pass are the necessary arguments to the parent function. self
is essentially “extra”, and is already “implemented” in the parent’s __init__
constructor method.
TL;DR: Yes, self
should be left off of the argument list when making super()
calls.
Dan B on Nov. 13, 2020
What motivates the decision to create a separate PayrollSystem
class rather than putting the calculate_payroll
method into the Employee
class?
Asking for a friend ;-)
davedanger on Nov. 16, 2020
Are you perhaps actually an AI? Explanations are decelerated just the right amount, understandibly methodical, and communicated perfectly. Thanks!
davedanger on Nov. 16, 2020
The above was stated in the spirit of humor. These tutorials in particular, clear up a lot of the mystery concerning classes and inheritance.
pyglet on Jan. 6, 2021
I’m a bit late to the party, but have the exact same issue as @KoJans:
My program.py only works if I delete self
from the
def calculate_payroll()
in the PayrollSystem class.
Does anyone mind explaining why? (it’s unclear to me whether @Roy Telles’ TL;DR meant yes, you need to delete self
in super().calculate_payroll()
or in the PayrollSystem class method definition.)
pyglet on Jan. 7, 2021
Oh boy, found the mistake I was making: I instantiated hr.PayrollSystem
instead of hr.PayrollSystem()
.
alazejha on May 23, 2021
I work on Jupyter notebook, and save both hr.py and program.py in one virtual directory. when I run the program.py it stops at the first line, with error; no module named hr.
Bartosz Zaczyński RP Team on May 24, 2021
@alazejha What do you mean by a “virtual” directory?
alazejha on May 24, 2021
Sorry if I’m not correctly decribing this, I run in shell:
python3 -m venv env
source env/bin/activate
jupyter notebook
and then I work in popped up jupyter notebook. I saved hr.py
, and wanted to import it, within program.py
, and it says that there’s no module hr
.
alazejha on May 24, 2021
Ah, I see that it’s automatically saved as hr.ipynb
. How can I save it as .py
?
Bartosz Zaczyński RP Team on May 25, 2021
@alazejha You can’t. Jupyter notebooks aren’t the same as Python source files. If you take a look at what’s inside, then you’ll see a JSON document with your Jupyter Notebook cells. You can create regular Python files separately and import them to your notebook, though.
bob on Oct. 24, 2021
It is confusing when the UML diagram does not match the implementation.
Please update the UML diagram to include the following attributes,
* SalaryEmployee: weekly_salary
* HourlyEmployee: hours_worked
* HourlyEmployee: hour_rate
* CommissionEmployee: commission
and also include the PayrollSystem
class.
rafalcode on Jan. 11, 2022
@Dan B: What motivates the decision to create a separate PayrollSystem class rather than putting the calculate_payroll method into the Employee class?
I expect it’s the big boss of the HR department. He/She arrives and says I want you to build me a payroll calculator, so it makes sense to looking like you’re making quick progress by starting with that class first.
muondude on Jan. 13, 2022
If I want to reference the parent class through
super().__init__(...)
in say the salaried employee subclass, but only cared about the employee ID, can I just use:
super().__init__(id)
or am I required to grab all the parent class attribute when using super()?
Another question: Let’s say I have a hierarchy where I want to compute a total as the sum of quantities computed by a method in each of the subclasses. Can I put the method to compute this sum in the parent class? It seems like I can’t since the parent isn’t aware of the children, whereas the children know their parent, or is this just my confusion? Or can the parent reference the children much like the car and engine example given?
I am asking since I’m currently working on a project and trying to determine a good class hierarchy for my code. My project this hierarchy is an example of aggregation. Each subclass can exist independently of the parent class, but the parent can’t exist without at least one subclass.
Bartosz Zaczyński RP Team on Jan. 14, 2022
@muondude When you use super()
to call a method defined in your parent class, then you have to pass all the required arguments even if you only care about one of them. When doing multiple inheritance in Python, it’s a common practice to declare your method signatures with the additional *args
and **kwargs
arguments that can work as a proxy, for example:
class Dad:
def __init__(self, eyes, *args, **kwargs):
super().__init__(*args, **kwargs)
self.eyes = eyes
class Mom:
def __init__(self, hair, *args, **kwargs):
super().__init__(*args, **kwargs)
self.hair = hair
class Child(Dad, Mom):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
child = Child(eyes="blue", hair="blonde")
As to your second question, it sounds like you’re trying to use the wrong tool. Inheritance models the relationship between types, while you probably want to keep a register of all the instances created at runtime and calculate their total sum. You can take advantage of Python’s duck typing to do so:
>>> class Foo:
... def subtotal(self):
... return 42
>>> class Bar:
... def subtotal(self):
... return 24
>>> register = [Foo(), Bar(), Foo(), Foo()]
>>> total = sum(x.subtotal() for x in register)
>>> total
150
It doesn’t matter if Foo
and Bar
belong to the same type hierarchy. They could be entirely unrelated types like in the example above.
muondude on Jan. 17, 2022
Wow. Thank you. That really helps. I appreciate these examples. As you can tell I’m winding my way through trying to learn OOP.
Regarding the total example, the context is I’m modeling noise in a sensor. There are different types of noise, each with its own unique model. I want to keep the models for each type separate. I’m trying to figure out if I need a parent class (Noise) for this set of specific noise classes. The parameters that define each noise model are read in from a JSON file using a mixin class that provides the method to read this file and create a parent object that holds all these parameters. I was thinking that the total method would be provided by the Noise parent class. I’m working on a UML diagram for this so I can post it if you think that might help.
Bartosz Zaczyński RP Team on Jan. 18, 2022
@muondude You’re welcome! Thanks to duck typing in Python, you most likely don’t need a parent class, but it might depend on what you’re trying to achieve. The UML diagrams would help me better understand your objective. Anyway, it sounds to me as if you’re coming from another object-oriented programming language, and you’re now trying to project its patterns onto Python, which may not be the most Pythonic or the most straightforward way to go.
muondude on Feb. 23, 2022
Bartosz: Are you on Slack (if yes ping me there)? Perhaps that might be a better place to continue the discussion. If you have another approach (e.g., email) I’m open. I can share my UML diagrams and more details on what I am trying to accomplish.
Sorry for the late reply. – Sam
Bartosz Zaczyński RP Team on Feb. 23, 2022
@muondude I am on Slack, but we don’t do 1:1 support. You’re better off asking your question and providing the context to a wider audience using one of the channels on Slack. Someone else may be able to give you a better answer or give it to you sooner.
rschuster3 on Dec. 20, 2022
Obviously it wasn’t intentional, but as a femme techie I found it darkly humorous that even in this hypothetical example the woman gets paid less.
Martin Breuss RP Team on Jan. 2, 2023
Ugh… Good point rschuster3 😢 Art imitating life or life imitating art?
jchurchi on Nov. 17, 2023
Please review my code comment here below. If I commented out the first indented line and uncommented the second it works the same and still gets the weekly salary from the same parent class so why call super again when I’ve already set it in the __init__
function. Am I wrong to assume it works the exact same way. Nothing gained using super (nothing lost either I suppose). I’m just curious.
def calculate_payroll(self):
fixed = super().calculate_payroll()
# fixed = self.weekly_salary
# The above works too so I'm not sure what he has gained
# by doing it using "super().calculate_payroll()".
return fixed + self.commission
Become a Member to join the conversation.
Zarata on April 15, 2020
Should “weekly_salary” show up in the UML diagram somewhere? (I’m 7 min into video)