Defining a function and calling it from several places saves you from having to copy and paste source code. Not duplicating code is a good practice, because if you need to change it (either for a bug fix or to add new features), you only need to change it in one place. Without duplicate code, the program is also shorter and easier to read.
Similar to functions, inheritance is a code reuse technique that you can apply to classes. It’s the act of putting classes into parent-child relationships in which the child class inherits a copy of the parent class’s methods, freeing you from duplicating a method in multiple classes.
Many programmers think inheritance is overrated or even dangerous because of the added complexity that large webs of inherited classes add to a program. Blog posts with titles like “Inheritance Is Evil” are not entirely off the mark; inheritance is certainly easy to overuse. But limited use of this technique can be a huge time-saver when it comes to organizing your code.
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:
1 class ParentClass:
2 def printHello(self):
print('Hello, world!')
3 class ChildClass(ParentClass):
def someNewMethod(self):
print('ParentClass objects don't have this method.')
4 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()
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
subclassesParentClass
, 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.
It’s common to say that parent-child classes represent “is a” relationships. 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 inheritance 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.
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 display 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.
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 different 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 organize our code into separate classes to handle these different use cases.
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 original 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."""
1 boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard.
xCanWin = False
oCanWin = False
2 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:
3 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
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
dictionary 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 overriding 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.
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 favorable 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:
WizCoin
object “has an” amount of galleon, sickle, and knut coins.TTTBoard
object “has a” set of nine spaces.MiniBoard
object “is a” TTTBoard
object, so it also “has a” set of nine spaces.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 inheritance, it could create some awkward code:
import wizcoin
1 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 wizard 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 methods 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.
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 favoring 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() # Use this engine by default.
--snip--
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.
When we need to know the type of an object, we can pass the object to the built-in type()
function, as described in the previous chapter. But if we’re doing a type check of an object, it’s a better idea to use the more flexible isinstance()
built-in function. The isinstance()
function will return True
if the object is of the given class or a subclass of the given class. Enter the following into the interactive shell:
>>> class ParentClass:
... pass
...
>>> class ChildClass(ParentClass):
... pass
...
>>> parent = ParentClass() # Create a ParentClass object.
>>> child = ChildClass() # Create a ChildClass object.
>>> isinstance(parent, ParentClass)
True
>>> isinstance(parent, ChildClass)
False
1 >>> isinstance(child, ChildClass)
True
2 >>> isinstance(child, ParentClass)
True
Notice that isinstance()
indicates that the ChildClass
object in child
is an instance of ChildClass
1 and an instance of ParentClass
2. This makes sense, because a ChildClass
object “is a” kind of ParentClass
object.
You can also pass a tuple of class objects as the second argument to see whether the first argument is one of any of the classes in the tuple:
>>> isinstance(42, (int, str, bool)) # True if 42 is an int, str, or bool.
True
The less commonly used issubclass()
built-in function can identify whether the class object passed for the first argument is a subclass of (or the same class as) the class object passed for the second argument:
>>> issubclass(ChildClass, ParentClass) # ChildClass subclasses ParentClass.
True
>>> issubclass(ChildClass, str) # ChildClass doesn't subclass str.
False
>>> issubclass(ChildClass, ChildClass) # ChildClass is ChildClass.
True
As you can with isinstance()
, you can pass a tuple of class objects as the second argument to issubclass()
to see whether the first argument is a subclass of any of the classes in the tuple. The key difference between isinstance()
and issubclass()
is that issubclass()
is passed two class objects, whereas isinstance()
is passed an object and a class object.
Class methods are associated with a class rather than with individual objects, like regular methods are. You can recognize a class method in code when you see two markers: the @classmethod
decorator before the method’s def
statement and the use of cls
as the first parameter, as shown in the following example.
class ExampleClass:
def exampleRegularMethod(self):
print('This is a regular method.')
@classmethod
def exampleClassMethod(cls):
print('This is a class method.')
# Call the class method without instantiating an object:
ExampleClass.exampleClassMethod()
obj = ExampleClass()
# Given the above line, these two lines are equivalent:
obj.exampleClassMethod()
obj.__class__.exampleClassMethod()
The cls
parameter acts like self
except self
refers to an object, but the cls
parameter refers to an object’s class. This means that the code in a class method cannot access an individual object’s attributes or call an object’s regular methods. Class methods can only call other class methods or access class attributes. We use the name cls
because class
is a Python keyword, and just like other keywords, such as if
, while
, or import
, we can’t use it for parameter names. We often call class attributes through the class object, as in ExampleClass.exampleClassMethod()
. But we can also call them through any object of the class, as in obj.exampleClassMethod()
.
Class methods aren’t commonly used. The most frequent use case is to provide alternative constructor methods besides __init__()
. For example, what if a constructor function could accept either a string of data the new object needs or a string of a filename that contains the data the new object needs? We don’t want the list of the __init__()
method’s parameters to be lengthy and confusing. Instead let’s use class methods to return a new object.
For example, let’s create an AsciiArt
class. As you saw in Chapter 14, ASCII art uses text characters to form an image.
class AsciiArt:
def __init__(self, characters):
self._characters = characters
@classmethod
def fromFile(cls, filename):
with open(filename) as fileObj:
characters = fileObj.read()
return cls(characters)
def display(self):
print(self._characters)
# Other AsciiArt methods would go here...
face1 = AsciiArt(' _______\n' +
'| . . |\n' +
'| \\___/ |\n' +
'|_______|')
face1.display()
face2 = AsciiArt.fromFile('face.txt')
face2.display()
The AsciiArt
class has an __init__()
method that can be passed the text characters of the image as a string. It also has a fromFile()
class method that can be passed the filename string of a text file containing the ASCII art. Both methods create AsciiArt
objects.
When you run this program and there is a face.txt file that contains the ASCII art face, the output will look something like this:
_______
| . . |
| \___/ |
|_______|
_______
| . . |
| \___/ |
|_______|
The fromFile()
class method makes your code a bit easier to read, compared to having __init__()
do everything.
Another benefit of class methods is that a subclass of AsciiArt
can inherit its fromFile()
method (and override it if necessary). This is why we call cls
(characters)
in the AsciiArt
class’s fromFile()
method instead of AsciiArt(characters)
. The cls()
call will also work in subclasses of AsciiArt
without modification because the AsciiArt
class isn’t hardcoded into the method. But an AsciiArt()
call would always call AsciiArt
class’s __init__()
instead of the subclass’s __init__()
. You can think of cls
as meaning “an object representing this class.”
Keep in mind that just as regular methods should always use their self
parameter somewhere in their code, a class method should always use its cls
parameter. If your class method’s code never uses the cls
parameter, it’s a sign that your class method should probably just be a function.
A class attribute is a variable that belongs to the class rather than to an object. We create class attributes inside the class but outside all methods, just like we create global variables in a .py file but outside all functions. Here’s an example of a class attribute named count
, which keeps track of how many CreateCounter
objects have been created:
class CreateCounter:
count = 0 # This is a class attribute.
def __init__(self):
CreateCounter.count += 1
print('Objects created:', CreateCounter.count) # Prints 0.
a = CreateCounter()
b = CreateCounter()
c = CreateCounter()
print('Objects created:', CreateCounter.count) # Prints 3.
The CreateCounter
class has a single class attribute named count
. All CreateCounter
objects share this attribute rather than having their own separate count
attributes. This is why the CreateCounter.count += 1
line in the constructor function can keep count of every CreateCounter
object created. When you run this program, the output will look like this:
Objects created: 0
Objects created: 3
We rarely use class attributes. Even this “count how many CreateCounter
objects have been created” example can be done more simply by using a global variable instead of a class attribute.
A static method doesn’t have a self
or cls
parameter. Static methods are effectively just functions, because they can’t access the attributes or methods of the class or its objects. Rarely, if ever, do you need to use static methods in Python. If you do decide to use one, you should strongly consider just creating a regular function instead.
We define static methods by placing the @staticmethod
decorator before their def
statements. Here is an example of a static method.
class ExampleClassWithStaticMethod:
@staticmethod
def sayHello():
print('Hello!')
# Note that no object is created, the class name precedes sayHello():
ExampleClassWithStaticMethod.sayHello()
There would be almost no difference between the sayHello()
static method in the ExampleClassWithStaticMethod
class and a sayHello()
function. In fact, you might prefer to use a function, because you can call it without having to enter the class name beforehand.
Static methods are more common in other languages that don’t have Python’s flexible language features. Python’s inclusion of static methods imitates the features of other languages but doesn’t offer much practical value.
You’ll rarely need class methods, class attributes, and static methods. They’re also prone to overuse. If you’re thinking, “Why can’t I just use a function or global variable instead?” this is a hint that you probably don’t need to use a class method, class attribute, or static method. The only reason this intermediate-level book covers them is so you can recognize them when you encounter them in code, but I’m not encouraging you to use them. They can be useful if you’re creating your own framework with an elaborate family of classes that are, in turn, expected to be subclassed by programmers using the framework. But you most likely won’t need them when you’re writing straightforward Python applications.
For more discussion on these features and why you do or don’t need them, read Phillip J. Eby’s post “Python Is Not Java” at https://dirtsimple.org/2004/12/python-is-not-java.html and Ryan Tomayko’s “The Static Method Thing” at https://tomayko.com/blog/2004/the-static-method-thing.
Explanations of OOP often begin with a lot of jargon, such as inheritance, encapsulation, and polymorphism. The importance of knowing these terms is overrated, but you should have at least a basic understanding of them. I already covered inheritance, so I’ll describe the other concepts here.
The word encapsulation has two common but related definitions. The first definition is that encapsulation is the bundling of related data and code into a single unit. To encapsulate means to box up. This is essentially what classes do: they combine related attributes and methods. For example, our WizCoin
class encapsulates three integers for knuts, sickles, and galleons into a single WizCoin
object.
The second definition is that encapsulation is an information hiding technique that lets objects hide complex implementation details about how the object works. You saw this in “Private Attributes and Private Methods” on page 282, where BankAccount
objects present deposit()
and withdraw()
methods to hide the details of how their _balance
attributes are handled. Functions serve a similar black box purpose: how the math.sqrt()
function calculates the square root of a number is hidden. All you need to know is that the function returns the square root of the number you passed it.
Polymorphism allows objects of one type to be treated as objects of another type. For example, the len()
function returns the length of the argument passed to it. You can pass a string to len()
to see how many characters it has, but you can also pass a list or dictionary to len()
to see how many items or key-value pairs it has, respectively. This form of polymorphism is called generic functions or parametric polymorphism, because it can handle objects of many different types.
Polymorphism also refers to ad hoc polymorphism or operator overloading, where operators (such as +
or *
) can have different behavior based on the type of objects they’re operating on. For example, the +
operator does mathematical addition when operating on two integer or float values, but it does string concatenation when operating on two strings. Operator overloading is covered in Chapter 17.
It’s easy to overengineer your classes using inheritance. As Luciano Ramalho states, “Placing objects in a neat hierarchy appeals to our sense of order; programmers do it just for fun.” We’ll create classes, subclasses, and sub-subclasses when a single class, or a couple of functions in a module, would achieve the same effect. But recall the Zen of Python tenet in Chapter 6 that simple is better than complex.
Using OOP allows you to organize your code into smaller units (in this case, classes) that are easier to reason about than one large .py file with hundreds of functions defined in no particular order. Inheritance is useful if you have several functions that all operate on the same dictionary or list data structure. In that case, it’s beneficial to organize them into a class.
But here are some examples of when you don’t need to create a class or use inheritance:
self
or cls
parameter, delete the class and use functions in place of the methods.As the non-OOP and OOP versions of the tic-tac-toe program in the previous chapter illustrate, it’s certainly possible to not use classes and still have a working, bug-free program. Don’t feel that you have to design your program as some complex web of classes. A simple solution that works is better than a complicated solution that doesn’t. Joel Spolsky writes about this in his blog post, “Don’t Let the Astronaut Architects Scare You” at https://www.joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you/.
You should know how object-oriented concepts like inheritance work, because they can help you organize your code and make development and debugging easier. Due to Python’s flexibility, the language not only offers OOP features, but also doesn’t require you to use them when they aren’t suited for your program’s needs.
Many programming languages limit classes to at most one parent class. Python supports multiple parent classes by offering a feature called multiple inheritance. For example, we can have an Airplane
class with a flyInTheAir()
method and a Ship
class with a floatOnWater()
method. We could then create a FlyingBoat
class that inherits from both Airplane
and Ship
by listing both in the class
statement, separated by commas. Open a new file editor window and save the following as flyingboat.py:
class Airplane:
def flyInTheAir(self):
print('Flying...')
class Ship:
def floatOnWater(self):
print('Floating...')
class FlyingBoat(Airplane, Ship):
pass
The FlyingBoat
objects we create will inherit the flyInTheAir()
and floatOnWater()
methods, as you can see in the interactive shell:
>>> from flyingboat import *
>>> seaDuck = FlyingBoat()
>>> seaDuck.flyInTheAir()
Flying...
>>> seaDuck.floatOnWater()
Floating...
Multiple inheritance is straightforward as long as the parent classes’ method names are distinct and don’t overlap. These sorts of classes are called mixins. (This is just a general term for a kind of class; Python has no mixin
keyword.) But what happens when we inherit from multiple complicated classes that do share method names?
For example, consider the MiniBoard
and HintTTTBoard
tic-tac-toe board classes from earlier in this chapter. What if we want a class that displays a miniature tic-tac-toe board and also provides hints? With multiple inheritance, we can reuse these existing classes. Add the following to the end of your tictactoe_oop.py file but before the if
statement that calls the main()
function:
class HybridBoard(HintBoard, MiniBoard):
pass
This class has nothing in it. It reuses code by inheriting from HintBoard
and MiniBoard
. Next, change the code in the main()
function so it creates a HybridBoard
object:
gameBoard = HybridBoard() # Create a TTT board object.
Both parent classes, MiniBoard
and HintBoard
, have a method named getBoardStr()
, so which one does HybridBoard
inherit? When you run this program, the output displays a miniature tic-tac-toe board but also provides hints:
--snip--
X.. 123
.O. 456
X.. 789
X can win in one more move.
Python seems to have magically merged the MiniBoard
class’s getBoardStr()
method and the HintBoard
class’s getBoardStr()
method to do both! But this is because I’ve written them to work with each other. In fact, if you switch the order of the classes in the HybridBoard
class’s class
statement so it looks like this:
class HybridBoard(MiniBoard, HintBoard):
you lose the hints altogether:
--snip--
X.. 123
.O. 456
X.. 789
To understand why this happens, you need to understand Python’s method resolution order (MRO) and how the super()
function actually works.
Our tic-tac-toe program now has four classes to represent boards, three with defined getBoardStr()
methods and one with an inherited getBoardStr()
method, as shown in Figure 16-2.
When we call getBoardStr()
on a HybridBoard
object, Python knows that the HybridBoard
class doesn’t have a method with this name, so it checks its parent class. But the class has two parent classes, both of which have a getBoardStr()
method. Which one gets called?
You can find out by checking the HybridBoard
class’s MRO, which is the ordered list of classes that Python checks when inheriting methods or when a method calls the super()
function. You can see the HybridBoard
class’s MRO by calling its mro()
method in the interactive shell:
>>> from tictactoe_oop import *
>>> HybridBoard.mro()
[<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>]
From this return value, you can see that when a method is called on HybridBoard
, Python first checks for it in the HybridBoard
class. If it’s not there, Python checks the HintBoard
class, then the MiniBoard
class, and finally the TTTBoard
class. At the end of every MRO list is the built-in object
class, which is the parent class of all classes in Python.
For single inheritance, determining the MRO is easy: just make a chain of parent classes. For multiple inheritance, it’s trickier. Python’s MRO follows the C3 algorithm, whose details are beyond the scope of this book. But you can determine the MRO by remembering two rules:
class
statement.If we call getBoardStr()
on a HybridBoard
object, Python checks the HybridBoard
class first. Then, because the class’s parents from left to right are HintBoard
and MiniBoard
, Python checks HintBoard
. This parent class has a getBoardStr()
method, so HybridBoard
inherits and calls it.
But it doesn’t end there: next, this method calls super().getBoardStr()
. Super is a somewhat misleading name for Python’s super()
function, because it doesn’t return the parent class but rather the next class in the MRO. This means that when we call getBoardStr()
on a HybridBoard
object, the next class in its MRO, after HintBoard
, is MiniBoard
, not the parent class TTTBoard
. So the call to super().getBoardStr()
calls the MiniBoard
class’s getBoardStr()
method, which returns the miniature tic-tac-toe board string. The remaining code in the HintBoard
class’s getBoardStr()
after this super()
call appends the hint text to this string.
If we change the HybridBoard
class’s class
statement so it lists MiniBoard
first and HintBoard
second, its MRO will put MiniBoard
before HintBoard
. This means HybridBoard
inherits getBoardStr()
from MiniBoard
, which doesn’t have a call to super()
. This ordering is what caused the bug that made the miniature tic-tac-toe board display without hints: without a super()
call, the MiniBoard
class’s getBoardStr()
method never calls the HintBoard
class’s getBoardStr()
method.
Multiple inheritance allows you to create a lot of functionality in a small amount of code but easily leads to overengineered, hard-to-understand code. Favor single inheritance, mixin classes, or no inheritance. These techniques are often more than capable of carrying out your program’s tasks.
Inheritance is a technique for code reuse. It lets you create child classes that inherit the methods of their parent classes. You can override the methods to provide new code for them but also use the super()
function to call the original methods in the parent class. A child class has an “is a” relationship with its parent class, because an object of the child class is a kind of object of the parent class.
In Python, using classes and inheritance is optional. Some programmers see the complexity that heavy use of inheritance creates as not worth its benefits. It’s often more flexible to use composition instead of inheritance, because it implements a “has a” relationship with an object of one class and objects of other classes rather than inheriting methods directly from those other classes. This means that objects of one class can have an object of another class. For example, a Customer
object could have a birthdate
attribute that is assigned a Date
object rather than the Customer
class subclassing Date
.
Just as type()
can return the type of the object passed to it, the isinstance()
and issubclass()
functions return type and inheritance information about the object passed to them.
Classes can have object methods and attributes, but they can also have class methods, class attributes, and static methods. Although these are rarely used, they can enable other object-oriented techniques that global variables and functions can’t provide.
Python lets classes inherit from multiple parents, although this can lead to code that is difficult to understand. The super()
function and a class’s methods figure out how to inherit methods based on the MRO. You can view a class’s MRO in the interactive shell by calling the mro()
method on the class.
This chapter and the previous one covered general OOP concepts. In the next chapter, we’ll explore Python-specific OOP techniques.