15.1. How Inheritance Works

To create a new child class, you put the name of the existing parent class in between parentheses in the class statement. To practice creating a child class, open a new file editor window and enter the following code; save it as inheritanceExample.py:

>>> class ParentClass:
>>>
>>>     def printHello(self):
>>>         print('Hello, world!')
>>> class ChildClass(ParentClass):
>>>     def someNewMethod(self):
>>>         print("ParentClass objects don't have this method.")
>>> class GrandchildClass(ChildClass):
>>>     def anotherNewMethod(self):
>>>         print('Only GrandchildClass objects have this method.')
>>> print('Create a ParentClass object and call its methods:')
>>> parent = ParentClass()
>>> parent.printHello()
>>> print('Create a ChildClass object and call its methods:')
>>> child = ChildClass()
>>> child.printHello()
>>> child.someNewMethod()
>>> print('Create a GrandchildClass object and call its methods:')
>>> grandchild = GrandchildClass()
>>> grandchild.printHello()
>>> grandchild.someNewMethod()
>>> grandchild.anotherNewMethod()
>>> print('An error:')
>>> parent.someNewMethod()
Create a ParentClass object and call its methods:
Hello, world!
Create a ChildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Create a GrandchildClass object and call its methods:
Hello, world!
ParentClass objects don't have this method.
Only GrandchildClass objects have this method.
An error:
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-2-dad14def73d8> in <module>
     22 grandchild.anotherNewMethod()
     23 print('An error:')
---> 24 parent.someNewMethod()


AttributeError: 'ParentClass' object has no attribute 'someNewMethod'

When you run this program, the output should look like this:

Create a ParentClass object and call its methods: Hello, world! Create a ChildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Create a GrandchildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Only GrandchildClass objects have this method. An error: Traceback (most recent call last): File "inheritanceExample.py", line 35, in <module> parent.someNewMethod() # ParentClass objects don't have this method. AttributeError: 'ParentClass' object has no attribute 'someNewMethod'

We’ve created three classes named ParentClass 1, ChildClass 3, and GrandchildClass 4. The ChildClass subclasses ParentClass , meaning that ChildClass will have all the same methods as ParentClass . We say that ChildClass inherits methods from ParentClass . Also, GrandchildClass subclasses ChildClass , so it has all the same methods as ChildClass and its parent, ParentClass .

Using this technique, we’ve effectively copied and pasted the code for the printHello() method 2 into the ChildClass and GrandchildClass classes. Any changes we make to the code in printHello() update not only ParentClass , but also ChildClass and GrandchildClass . This is the same as changing the code in a function updates all of its function calls. You can see this relationship in Figure 16-1. Notice that in class diagrams, the arrow is drawn from the subclass pointing to the base class. This reflects the fact that a class will always know its base class but won’t know its subclasses.

_images/%E9%80%89%E5%8C%BA_012.png

Figure 16-1: A hierarchical diagram (left) and Venn diagram (right) showing the relation- ships between the three classes and the methods they have

It’s common to say that parent-child classes represent “is a” relation- ships. A ChildClass object is a ParentClass object because it has all the same methods that a ParentClass object has, including some additional methods it defines. This relationship is one way: a ParentClass object is not a ChildClass object. If a ParentClass object tries to call someNewMethod() , which only exists for ChildClass objects (and the subclasses of ChildClass ), Python raises an AttributeError .

Programmers often think of related classes as having to fit into some real-world “is a” hierarchy. OOP tutorials commonly have parent, child, and grandchild classes of Vehicle  FourWheelVehicle  Car , Animal  Bird  Sparrow , or Shape  Rectangle  Square . But remember that the primary purpose of inheri- tance is code reuse. If your program needs a class with a set of methods that is a complete superset of some other class’s methods, inheritance allows you to avoid copying and pasting code.

We also sometimes call a child class a subclass or derived class and call a parent class the super class or base class. ## Overriding Methods Child classes inherit all the methods of their parent classes. But a child class can override an inherited method by providing its own method with its own code. The child class’s overriding method will have the same name as the parent class’s method.

To illustrate this concept, let’s return to the tic-tac-toe game we created in the previous chapter. This time, we’ll create a new class, MiniBoard , that subclasses TTTBoard and overrides getBoardStr() to provide a smaller drawing of the tic-tac-toe board. The program will ask the player which board style to use. We don’t need to copy and paste the rest of the TTTBoard methods because MiniBoard will inherit them.

Add the following to the end of your tictactoe_oop.py file to create a child class of the original TTTBoard class and then override the getBoardStr() method:

class MiniBoard(TTTBoard):

def getBoardStr(self):

# """Return a tiny text-representation of the board.""" # Change blank spaces to a '.'

for space in ALL_SPACES:
if self._spaces[space] == BLANK:

self._spaces[space] = '.'

boardStr = f'''

{self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123 {self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456 {self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789''' # Change '.' back to blank spaces.

for space in ALL_SPACES:
if self._spaces[space] == '.':

self._spaces[space] = BLANK

return boardStr

As with the getBoardStr() method for the TTTBoard class, the getBoardStr() method for MiniBoard creates a multiline string of a tic-tac-toe board to dis- play when passed to the print() function. But this string is much smaller, forgoing the lines between the X and O marks and using periods to indicate blank spaces.

Change the line in main() so it instantiates a MiniBoard object instead of a TTTBoard object:

>>> if input('Use mini board? Y/N: ').lower().startswith('y'):
>>>     gameBoard = MiniBoard() # Create a MiniBoard object.
>>> else:
>>>     gameBoard = TTTBoard() # Create a TTTBoard object.
Use mini board? Y/N:  n
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-5-e2097cc3092b> in <module>
      2     gameBoard = MiniBoard() # Create a MiniBoard object.
      3 else:
----> 4     gameBoard = TTTBoard() # Create a TTTBoard object.


NameError: name 'TTTBoard' is not defined

Other than this one line change to main() , the rest of the program works the same as before. When you run the program now, the output will look something like this:

Welcome to Tic-Tac-Toe! Use mini board? Y/N: y ... 123 ... 456 ... 789 What is X's move? (1-9) 1 X.. 123 ... 456 ... 789 What is O's move? (1-9) --snip-- XXX 123 .OO 456 O.X 789 X has won the game! Thanks for playing!

Your program can now easily have both implementations of these tic- tac-toe board classes. Of course, if you only want the mini version of the board, you could simply replace the code in the getBoardStr() method for TTTBoard . But if you need both, inheritance lets you easily create two classes by reusing their common code.

If we didn’t use inheritance, we could have, say, added a new attribute to TTTBoard called useMiniBoard and put an if - else statement inside getBoardStr() to decide when to show the regular board or the mini one. This would work well for such a simple change. But what if the MiniBoard subclass needed to override 2, 3, or even 100 methods? What if we wanted to create several dif- ferent subclasses of TTTBoard ? Not using inheritance would cause an explosion of if - else statements inside our methods and a large increase in our code’s complexity. By using subclasses and overriding methods, we can better orga- nize our code into separate classes to handle these different use cases. ## The super() Function A child class’s overridden method is often similar to the parent class’s method. Even though inheritance is a code reuse technique, overriding a method might cause you to rewrite the same code from the parent class’s method as part of the child class’s method. To prevent this duplicate code, the built-in super() function allows an overriding method to call the origi- nal method in the parent class. For example, let’s create a new class called HintBoard that subclasses TTTBoard . The new class overrides getBoardStr() , so after drawing the tic- tac-toe board, it also adds a hint if either X or O could win on their next move. This means that the HintBoard class’s getBoardStr() method has to do all the same tasks that the TTTBoard class’s getBoardStr() method does to draw the tic-tac-toe board. Instead of repeating the code to do this, we can use super() to call the TTTBoard class’s getBoardStr() method from the HintBoard class’s getBoardStr() method. Add the following to the end of your ­ tictactoe _oop.py file:

>>> class HintBoard(TTTBoard):
>>>     def getBoardStr(self):
>>> # """Return a text-representation of the board with hints."""
>>>
>>>         boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard.
>>>
>>>         xCanWin = False
>>>         oCanWin = False
>>>         originalSpaces = self._spaces # Backup _spaces.
>>>         for space in ALL_SPACES: # Check each space:
>>> # Simulate X moving on this space:
>>>             self._spaces = copy.copy(originalSpaces)
>>>             if self._spaces[space] == BLANK:
>>>                 self._spaces[space] = X
>>>             if self.isWinner(X):
>>>                 xCanWin = True
>>> # Simulate O moving on this space:
>>>             self._spaces = copy.copy(originalSpaces)
>>>             if self._spaces[space] == BLANK:
>>>                 self._spaces[space] = O
>>>             if self.isWinner(O):
>>>                 oCanWin = True
>>>         if xCanWin:
>>>                 boardStr += '\nX can win in one more move.'
>>>         if oCanWin:
>>>             boardStr += '\nO can win in one more move.'
>>>         self._spaces = originalSpaces
>>>         return boardStr
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-6-d8cfbcd888e7> in <module>
----> 1 class HintBoard(TTTBoard):
      2     def getBoardStr(self):
      3 # """Return a text-representation of the board with hints."""
      4
      5         boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard.


NameError: name 'TTTBoard' is not defined

First, super().getBoardStr() 1 runs the code inside the parent TTTBoard class’s getBoardStr() , which returns a string of the tic-tac-toe board. We save this string in a variable named boardStr for now. With the board string created by reusing TTTBoard class’s getBoardStr() , the rest of the code in this method handles generating the hint. The getBoardStr() method then sets xCanWin and oCanWin variables to False , and backs up the self._spaces diction- ary to an originalSpaces variable 2. Then a for loop loops over all board spaces from ‘1’ to ‘9’ . Inside the loop, the self._spaces attribute is set to a copy of the originalSpaces dictionary, and if the current space being looped on is blank, an X is placed there. This simulates X moving on this blank space for its next move. A call to self.isWinner() will determine if this would be a winning move, and if so, xCanWin is set to True . Then these steps are repeated for O to see whether O could win by moving on this space 3. This method uses the copy module to make a copy of the dictionary in self._spaces , so add the following line to the top of tictactoe.py:

>>> import copy

Next, change the line in main() so it instantiates a HintBoard object instead of a TTTBoard object:

gameBoard = HintBoard() # Create a TTT board object.

Other than this one line change to main() , the rest of the program works exactly as before. When you run the program now, the output will look something like this:

>>> Welcome to Tic-Tac-Toe!
>>> --snip--
>>> X| |
>>> 1 2 3
>>> -+-+-
>>> | |O 4 5 6
>>> -+-+-
>>> | |X 7 8 9
>>> X can win in one more move.
>>> What is O's move? (1-9)
>>> 5
>>> X| |
>>> 1 2 3
>>> -+-+-
>>> |O|O 4 5 6
>>> -+-+-
>>> | |X 7 8 9
>>> O can win in one more move.
>>> --snip--
>>> The game is a tie!
>>> Thanks for playing!

At the end of the method, if xCanWin or oCanWin is True , an additional message stating so is added to the boardStr string. Finally, boardStr is returned.

Not every overridden method needs to use super() ! If a class’s over- riding method does something completely different from the overridden method in the parent class, there’s no need to call the overridden method using super() . The super() function is especially useful when a class has more than one parent method, as explained in “Multiple Inheritance” later in this chapter. ## Favor Composition Over Inheritance Inheritance is a great technique for code reuse, and you might want to start using it immediately in all your classes. But you might not always want the base and subclasses to be so tightly coupled. Creating multiple levels of inheritance doesn’t add organization so much as bureaucracy to your code.

Although you can use inheritance for classes with “is a” relationships (in other words, when the child class is a kind of the parent class), it’s often favor- able to use a technique called composition for classes with “has a” relationships. Composition is the class design technique of including objects in your class rather than inheriting those objects’ class. This is what we do when we add attributes to our classes. When designing your classes using inheritance, favor composition instead of inheritance. This is what we’ve been doing with all the examples in this and the previous chapter, as described in the following list:

  • A WizCoin object “has an” amount of galleon, sickle, and knut coins.

  • A TTTBoard object “has a” set of nine spaces.

  • A MiniBoard object “is a” TTTBoard object, so it also “has a” set of nine spaces.

  • A HintBoard object “is a” TTTBoard object, so it also “has a” set of nine spaces.

Let’s return to our WizCoin class from the previous chapter. If we created a new WizardCustomer class to represent customers in the wizarding world, those customers would be carrying an amount of money, which we could represent through the WizCoin class. But there is no “is a” relationship between the two classes; a WizardCustomer object is not a kind of WizCoin object. If we used inheri- tance, it could create some awkward code:

import wizcoin class WizardCustomer(wizcoin.WizCoin):

def __init__(self, name):

self.name = name

super().__init__(0, 0, 0)

wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.value()} knuts worth of money.') print(f'{wizard.name}'s coins weigh {wizard.weightInGrams()} grams.')

In this example, WizardCustomer inherits the methods of a WizCoin 1 object, such as value() and weightInGrams() . Technically, a WizardCustomer that inherits from WizCoin can do all the same tasks that a WizardCustomer that includes a WizCoin object as an attribute can. But the wizard.value() and wizard.weightInGrams() method names are misleading: it seems like they would return the wizard’s value and weight rather than the value and weight of the wizard’s coins. In addition, if we later wanted to add a weightInGrams() method for the wizard’s weight, that method name would already be taken.

It’s much better to have a WizCoin object as an attribute, because a wiz- ard customer “has a” quantity of wizard coins:

import wizcoin class WizardCustomer: def __init__(self, name): self.name = name 1 self.purse = wizcoin.WizCoin(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.purse.value()} knuts worth of money.')

print(f'{wizard.name}'s coins weigh {wizard.purse.weightInGrams()} grams.')

Instead of making the WizardCustomer class inherit methods from WizCoin , we give the WizardCustomer class a purse attribute 1, which contains a WizCoin object. When you use composition, any changes to the WizCoin class’s meth- ods won’t change the WizardCustomer class’s methods. This technique offers more flexibility in future design changes for both classes and leads to more maintainable code. ## Inheritance’s Downside The primary downside of inheritance is that any future changes you make to parent classes are necessarily inherited by all its child classes. In most cases, this tight coupling is exactly what you want. But in some instances, your code requirements won’t easily fit your inheritance model.

For example, let’s say we have Car , Motorcycle , and LunarRover classes in a vehicle simulation program. They all need similar methods, such as startIgnition() and changeTire() . Instead of copying and pasting this code into each class, we can create a parent Vehicle class and have Car , Motorcycle , and LunarRover inherit it. Now if we need to fix a bug in, say, the changeTire() method, there’s only one place we need to make the change. This is especially helpful if we have dozens of different vehicle- related classes inheriting from Vehicle . The code for these classes would look like this:

class Vehicle:
def __init__(self):

print('Vehicle created.')

def startIgnition(self):

pass # Ignition starting code goes here.

def changeTire(self):

pass # Tire changing code goes here.

class Car(Vehicle):
def __init__(self):

print('Car created.')

class Motorcycle(Vehicle):
def __init__(self):

print('Motorcycle created.')

class LunarRover(Vehicle):
def __init__(self):

print('LunarRover created.')

But all future changes to Vehicle will affect these subclasses as well. What happens if we need a changeSparkPlug() method? Cars and motorcycles have combustion engines with spark plugs, but lunar rovers don’t. By favor- ing composition over inheritance, we can create separate CombustionEngine and ElectricEngine classes. Then we design the Vehicle class so it “has an” engine attribute, either a CombustionEngine or ElectricEngine object, with the appropriate methods:

class CombustionEngine:
def __init__(self):

print('Combustion engine created.')

def changeSparkPlug(self):

pass # Spark plug changing code goes here.

class ElectricEngine:
def __init__(self):

print('Electric engine created.')

class Vehicle:
def __init__(self):

print('Vehicle created.') self.engine = CombustionEngine()

--snip-- # Use this engine by default. class LunarRover(Vehicle):

def __init__(self):

print('LunarRover created.') self.engine = ElectricEngine()

This could require rewriting large amounts of code, particularly if you have several classes that inherit from a preexisting Vehicle class: all the vehicleObj.changeSparkPlug() calls would need to become vehicleObj.engine .changeSparkPlug() for every object of the Vehicle class or its subclasses. Because such a sizeable change could introduce bugs, you might want to simply have the changeSparkPlug() method for LunarVehicle do nothing. In this case, the Pythonic way is to set changeSparkPlug to None inside the LunarVehicle class:

class LunarRover(Vehicle):

changeSparkPlug = None def __init__(self):

print('LunarRover created.')

>>> The changeSparkPlug = None line follows the syntax described in “Class
>>> Attributes” later in this chapter. This overrides the changeSparkPlug() method
>>> inherited from Vehicle , so calling it with a LunarRover object causes an error:
>>> myVehicle = LunarRover()
LunarRover created.
>>> myVehicle.changeSparkPlug()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

This error allows us to fail fast and immediately see a problem if we try to call this inappropriate method with a LunarRover object. Any child classes of LunarRover also inherit this None value for changeSparkPlug() . The TypeError: ‘NoneType’ object is not callable error message tells us that the programmer of the LunarRover class intentionally set the changeSparkPlug() method to None . If no such method existed in the first place, we would have received a NameError: name ‘changeSparkPlug’ is not defined error message.

Inheritance can create classes with complexity and contradiction. It’s often favorable to use composition instead.