Object-Oriented Python at the Hogwarts School of Codecraft and Algorithmancy
---
Year 5:
Inheritance
---
The students are growing up! This Year they'll learn about a key property of OOP - how to create a class that inherits from another one
Here's the code so far
We've got four classes:
• Wizard
• House
• Wand
• Spell
Each one has its own attributes — these are data attributes and methods
The classes represent "things" as seen from a human's perspective. Each class contains all the data attributes and methods needed by an object
Now, at Hogwarts School of Codecraft & Algorithmancy there are students & professors
They have different things, such as "subject taught" or "mark exam" for professors and "subjects studied" or "take exam" for students
However, all professors & students are wizards
It would seem wasteful to have to define two separate classes for Professor and Student which have a lot of code in common but also some differences
Indeed, this is not an efficient way to create two similar classes
Instead, we'll create the two new classes `Professor` and `Student` but we'll make these classes _inherit_ from `Wizard`
All professors are wizards but not all wizards are professors
All students are wizards but not all wizards are students
When you create a class that inherits from another one, the _subclass_ or _derived class_ starts off having the same attributes as the _superclass_ or _base class_
Then, you can add attributes or even change some of the existing ones, as you'll see in this Year's curriculum
Let's create a `Professor` class that inherits from `Wizard`
I'm showing the `Wizard` class in full as well as the new `Professor` class
Let's look at what's different in how we define this new class
First, you'll notice that there are parentheses after the name of the class
The new class inherits from the class named in the parentheses
Therefore, `Professor` inherits from `Wizard`
Every professor is also a wizard
The `Professor` class has its own `.__init__()` method
In addition to `self`, this method also has `name`, `patronus`, and `birth_year` as is the case in the `Wizard` class
However, there's an additional parameter: `subject`
This is the subject that the professor teaches
The first line in the `.__init__()` method in the `Professor` class may look weird. Let's break it down.
You may recall we used the term superclass to define the class we're inheriting from
`super()` allows you to access this superclass
And since `Professor` is also a member of `Wizard`, you need to call the `.__init__()` method for the superclass
This is what `super().__init__()` does
`super().__init__()` only takes 3 arguments since it's the `Wizard` initialisation method, which only has 3 parameters
Therefore, the line:
`super().__init__()` makes sure that when you create an instance of `Professor`, it is also an instance of `Wizard`
Incidentally, another name for the superclass or base class is the parent class
And the subclass or derived class can be called a child class
Lots of different names for the same thing!
We finish off the `.__init__()` method of the `Professor` class by assigning the data attribute that's unique to the `Professor` class,: `.subject`
The parent class, `Wizard`, does not have this attribute
In summary, we make sure `Professor` is a `Wizard` by calling `super().__init__()`, then we add what's unique to `Professor`
It is also possible to change existing attributes. We'll see this in action soon
Let's add a method that's unique to the `Professor` subclass, which the `Wizard` superclass doesn't have
`.assess_student()` has two parameters in addition to `self`, which is always the first in an instance method
We'll finish the code for this method later
`.assess_student()` is a method which belongs to the `Professor` class but not to the `Wizard` class
If you try to call this method on a wizard who's not a professor, it won't work.
However, you can also redefine a method which the parent class or superclass already has
`.assign_wand()` in the `Professor` class is different from the one in the parent class
In this case, it still uses the method in the parent class since you call `super().assign_wand()`. This calls the method in `Wizard`
You also choose to increase the professor's skill
The method in the child class `Professor` _overrides_ the method with the same name in the parent class `Wizard`
If you want this method to be significantly different from the one in the parent class, you don't need to use `super()` to call the method in the parent class
Let's create the `Student` class which also inherits from `Wizard` since all students are wizards
• There's an additional parameter in `__init__()`, and a matching data attribute, `.year`
• There's also a `.subject_grades` data attribute which is a dictionary. It will store subjects as keys and the grade the student gets as their values
• `.assign_subjects()` and `.take_exam()` are new methods
In `.assign_subjects()`, you use a dictionary comprehension to convert the names of the subjects in the iterable `subjects` into the dictionary `.subject_grades`. The value is `None` initially
`.take_exam()` assigns a grade for that subject to the student
• `assign_house_using_sorting_hat()` is also a new method. We'll work on this later on (final year) and once the sorting hat chooses a house at random, then the object's `.assign_house()` method, which it inherits from `Wizard`, is called
Finally, we update the method `.assess_student()` in the `Professor` class, which we hadn't completed earlier
Note that when we loop through a dictionary, such as `student.subject_grades`, we're looping through its keys. In this case, that's the subject names
Let's try out these new subclasses and their methods in `making_magic.py`
`harry` and `hermione` are now instances of `Student` and not `Wizard`
…although recall they're still wizards!
`isinstance(harry, Wizard)` is still `True`!
We're using strings to represent subject names in this example to avoid the code from getting too long and complex in this series
You could create a class called `Subject` if you prefer!
Note also how the call:
`snape.assess_student(harry, 20)`
doesn't have a subject name
Let's follow what's happening here:
The first object to take in to account is `snape`, an instance of `Professor`
You call its `.assess_student()` method and pass `harry` to it, an instance of `Student`
`.assess_student()` has access to:
• `snape` (this is `self` in the class definition) and all data attributes `snape` has, including `.subject`
• `harry` and all its attributes
`.assess_student()` passes the subject name, which is an attribute of `snape`, to `harry` through the `Student` method `.take_exam()`
Note how data is moved from one object to another, but this happens "behind the scenes" from the perspective of the `making_magic.py` script
Once classes are written, they abstract away the details
The user of the classes doesn't need to worry about how they're implemented
---
Terminology Corner
---
• Subclass or derived class or child class: A class which inherits from another one
• Superclass or base class or parent class: A class from which other classes inherit
And you've made it to the end of Year 5
You've earned your break
---
Year 6: Special Methods (Dunder methods)