Define a function, and call it from several places, saving you the hassle of copying and pasting source code. Not duplicating code is a good practice, because if you need to modify it (whether to fix a bug or add a new feature), you only need to modify it in one place. There is no repeated code, and programs are shorter and easier to read.
Similar to functions, inheritance is a code reuse technique that you can apply to classes. This is the act of placing classes in a parent-child relationship, where the child class inherits a copy of the parent's method, saving you from having to duplicate a method in multiple classes.
Many programmers consider inheritance to be overrated, even dangerous, because a large number of inherited classes increases program complexity. A blog post with the title "Inheritance is Evil" isn't entirely rambling; inheritance is certainly prone to overuse. But limited use of this technique can save a lot of time when organizing code.
How inheritance works
To create a new subclass, put the name of an existing parent class in class
parentheses in the statement. To practice subclassing, open a new file editor window and enter the following code; save as inheritanceExample.py
:
class ParentClass: # 1
def printHello(self): # 2
print('Hello, world!')
class ChildClass(ParentClass): # 3
def someNewMethod(self):
print('ParentClass objects don't have this method.')
class GrandchildClass(ChildClass): # 4
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 running the 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 created three classes named ParentClass
1, ChildClass
3 and 4. A subclass means it will have all the same methods as . We say inherited methods . In addition, the subclass inherits from , so it has all the same methods as its parent class .GrandchildClass
ChildClass
ParentClass
ChildClass
ParentClass
ChildClass
ParentClass
GrandchildClass
ChildClass
ChildClass
ParentClass
Using this technique, we've effectively printHello()
copied and pasted the code for method 2 into the ChildClass
and GrandchildClass
class. Any changes we make to printHello()
the code will not only update ParentClass
, but also update ChildClass
and GrandchildClass
. This is the same as changing the code in a function will update all its function calls. You can see this relationship in Figure 16-1. Note that in the class diagram, the arrows point from subclasses to base classes. This reflects the fact that a class is always aware of its base classes, but not its subclasses.
Figure 16-1: A hierarchy diagram (left) and a Venn diagram (right) showing the relationship between the three classes and the methods they have
It is often said that parent-child classes represent an "is-a" relationship. An ChildClass
object is an ParentClass
object because it has ParentClass
all the same methods as an object, including some additional methods it defines. This relationship is one-way: ParentClass
objects are not ChildClass
objects. If an ParentClass
object tries to call someNewMethod()
, which only exists on ChildClass
objects (and ChildClass
subclasses of ), Python throws one AttributeError
.
Programmers often think that related classes must fit into a real-world "is" hierarchy. OOP tutorials generally have Vehicle``FourWheelVehicle▶``Car``Animal``Bird``Sparrow
, or Shape``Rectangle``Square
the parent class, child class, grandson class. But remember, the main purpose of inheritance is code reuse. If your program requires a class with a set of methods that are an exact superset of other classes' methods, inheritance allows you to avoid copying and pasting code.
We also sometimes call subclasses subclasses or derived classes , and parent classes superclasses or base classes .
override method
A subclass inherits all the methods of its parent class. But subclasses can override inherited methods by providing their own methods with their own code. The overridden method of the subclass will have the same name as the method of the superclass.
To illustrate this concept, let's go back to the tic-tac-toe game we created in the previous chapter. This time, we'll create a new class MiniBoard
that inherits TTTBoard
and overrides getBoardStr()
to provide a smaller tic-tac-toe board. The program will ask the player which style of board to use. We don't need to copy and paste the rest of TTTBoard
the methods as MiniBoard
they will be inherited.
Add the following to tictactoe_oop.py
the end of your file to subclass the original TTTBoard
class and then override getBoardStr()
the methods:
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 methods TTTBoard
of the class , the method creates a multi-line string of a tic-tac-toe board that is displayed when passed to the function. But this string is much smaller, dropping the line between the X and O tokens, and using dots to represent spaces.getBoardStr()
MiniBoard
getBoardStr()
print()
Change main()
the line in so that it instantiates an MiniBoard
object instead of an 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.
Except for main()
this one line modification, the rest of the program is the same as before. When you run the program now, the output will look 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 implementations of these two tic-tac-toe board classes. Of course, if you only want the mini version of the board, you can simply replace the code TTTBoard
in getBoardStr()
the method. But if you need and , inheritance allows you to easily create two classes by reusing their common code.
If we weren't using inheritance, we could TTTBoard
add a useMiniBoard
new property called , and getBoardStr()
put a if-else
statement in it to decide when to show the regular panel or the mini-panel. For such a simple change, this will work fine. But what if MiniBoard
the subclass needs to override 2, 3, or even 100 methods? What if we wanted to create several different TTTBoard
subclasses? Not using inheritance would cause the statement explosion in our methods if-else
and greatly increase the complexity of the code. By using subclasses and overriding methods, we can better organize our code into separate classes to handle these different use cases.
super()
function
Subclass overridden methods are usually similar to superclass methods. Although inheritance is a code reuse technique, overriding a method can cause you to override the same code in a parent class method as part of a subclass method. To prevent this duplication of code, built-in super()
functions allow an overriding method to call the original method in the parent class.
For example, let's create a HintBoard
new class called , which is TTTBoard
a subclass of . The new class overrides getBoardStr()
, so after drawing the tic-tac-toe board, it also adds a hint if X or O wins on their next move. This means that the methods HintBoard
of the class getBoardStr()
must do all the same tasks as the methods TTTBoard
of the class that draw the tic-tac-toe board. Instead of duplicating code, getBoardStr()
we can do this by calling methods of a class super()
from methods HintBoard
of a class . Add the following to the end of your file:getBoardStr()
TTTBoard
getBoardStr()
tictactoe_oop.py
class HintBoard(TTTBoard):
def getBoardStr(self):
"""Return a text-representation of the board with hints."""
boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard. # 1
xCanWin = False
oCanWin = False
originalSpaces = self._spaces # Backup _spaces. # 2
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) # 3
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 getBoardStr()
, returning the string for the tic-tac-toe board. We temporarily save this string in a boardStr
variable called . The rest of the code in this method handles the generation of hints using the checkerboard string created by reusing TTTBoard
the class . The method getBoardStr()
then sets the and variable to and backs up the dictionary as variable 2. Then a loop loops over all the board spaces from to . Inside the loop, the attribute is set to a copy of the dictionary, and if the current space being looped is empty, an X is placed there. This simulates the next move of X on this empty space. A call to will determine if this is a winning move, and if so, will set it to . Then repeat these steps for O to see if O can move 3 on this space to win. This method uses the module to copy the dictionary in, so add the following line at the top of the :getBoardStr()
xCanWin
oCanWin
False
self._spaces
originalSpaces
for
'1'
'9'
self._spaces
originalSpaces
self.isWinner()
xCanWin
True
copy
self._spaces
tictactoe.py
import copy
Next, change main()
the line in so that it instantiates an HintBoard
object instead of an TTTBoard
object:
gameBoard = HintBoard() # Create a TTT board object.
Except for main()
this one line modification, the rest of the program is exactly the same as before. When you run the program now, the output will look 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 truexCanWin
, an additional statement message is added to the string. Finally, back again.oCanWin
True
boardStr
boardStr
Not every overridden method needs to be used super()
! If the overriding method of a class does something completely different from the overridden method in the parent class, there is no need to use super()
call the overridden method. Functions are especially useful when a class has more than one parent method super()
, as explained in "Multiple Inheritance" later in this chapter.
Prefer composition over inheritance
Inheritance is a great technique for code reuse, and you probably want to start using it in all your classes right away. But you may not always want base and subclasses to be so tightly coupled. Creating multiple levels of inheritance doesn't add organization to your code, it adds bureaucracy.
While you can use inheritance with classes that have an "is" relationship (in other words, when the subclass is a kind of parent class), it is often advantageous to use a technique called composition with classes that have a "is " relationship . Composition is a class design technique in which objects are contained within classes rather than classes that inherit those objects. This is what we do when we add properties to a class. When designing your classes using inheritance, favor composition over inheritance. This is what we did in all the examples in this and the previous chapter, as described in the table below:
- An object "has" a certain number of Galleon, Sickle, and Canute coins.
- An object "has" a set of nine spaces.
- An
MiniBoard
object is anTTTBoard
object, so it also "has" a set of nine spaces. - An
HintBoard
object is anTTTBoard
object, so it also "has" a set of nine spaces.
Let's go back to WizCoin
the classes from the previous chapter. If we create a new WizardCustomer
class to represent customers in the wizarding world, these customers will carry a certain amount of money, we can WizCoin
represent this money through the class. But there is no "is-one" relationship between the two classes; an WizardCustomer
object is not an WizCoin
object. If we use inheritance, it may result in some clumsy code:
import wizcoin
class WizardCustomer(wizcoin.WizCoin): # 1
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
inheriting WizCoin
methods of a 1 object, such as value()
and weightInGrams()
. Technically, inheriting from WizCoin
can WizardCustomer
accomplish the same task as including WizCoin
an object as a property . WizardCustomer
But the names of the wizard.value()
and wizard.weightInGrams()
methods are misleading: they seem to return the value and weight of the wizard, not the value and weight of the wizard coin. Also, if we later wanted to add a method to the wizard's weight weightInGrams()
, that method name would already be taken.
It's better to have an WizCoin
object as a property, since a wizard customer "has" a certain amount of wizard coins:
import wizcoin
class WizardCustomer:
def __init__(self, name):
self.name = name
self.purse = wizcoin.WizCoin(0, 0, 0) # 1
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.')
WizardCustomer
Instead of having the class inherit methods from , we WizCoin
gave WizardCustomer
the class an purse
attribute 1 , which contains an WizCoin
object. When using composition, WizCoin
any changes to the methods of the class will not change WizardCustomer
the methods of the class. This technique provides more flexibility for future design changes of the two classes and makes the code more maintainable.
Inherited Disadvantages
The main disadvantage of inheritance is that any future changes you make to the parent class are bound to be inherited by all its subclasses. In most cases, this tight coupling is exactly what you want. But in some cases, your code needs don't quite fit your inheritance model.
Car
For example, suppose we have , Motorcycle
and classes in a vehicle simulation program LunarRover
. They all require similar methods such as startIgnition()
and changeTire()
. Instead of copying and pasting this code into each class, we can create a parent class Vehicle
and have Car
, , Motorcycle
and inherit from it. LunarRover
Now, if we need to fix a bug in, say, a changeTire()
method, we only need to modify it in one place. This is especially useful if we have dozens of Vehicle
different vehicle-related classes that we inherit from. The code for these classes looks 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 Vehicle
changes to will affect these subclasses as well. What if we need a changeSparkPlug()
method? Cars and motorcycles have internal combustion engines with spark plugs, but lunar rovers do not. By favoring composition over inheritance, we can create individual CombustionEngine
and ElectricEngine
classes. We then designed Vehicle
the class so that it "has a" engine attribute, either a
CombustionEngine 或
ElectricEngine对象,使用适当的方法:
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 can require a lot of code rewriting, especially if you have several classes that inherit from pre-existing Vehicle
classes: for Vehicle
each object of the class or its subclasses, all vehicleObj.changeSparkPlug()
calls need to be turned into vehicleObj.engine.changeSparkPlug()
. Because such large changes can introduce errors, you may wish to simply make LunarVehicle
the changeSparkPlug()
method do nothing. In this case, the Python-style approach is to set it LunarVehicle
within the class to :changeSparkPlug
None
class LunarRover(Vehicle):
changeSparkPlug = None
def __init__(self):
print('LunarRover created.')
changeSparkPlug = None
line follows the syntax described in "Class Attributes" later in this chapter. This overrides the method Vehicle
inherited from changeSparkPlug()
, so LunarRover
calling it with an object will result in 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 spot the problem immediately if we try to LunarRover
call this inappropriate method with an object. Any LunarRover
subclasses also changeSparkPlug()
inherit this None
value. TypeError: 'NoneType' object is not callable
The error message tells us that LunarRover
the programmer of the class intentionally changeSparkPlug()
set the method to None
. If there wasn't such a method in the first place, we'd get an NameError: name 'changeSparkPlug' is not defined
error message.
Inheritance can create complex and contradictory classes. It is often advantageous to use text instead.
isinstance()
and issubclass()
functions
When we need to know the type of an object, we can pass the object to a built-in type()
function, as described in the previous chapter. But if we're doing type checking on an object, isinstance()
it's a better idea to use the more flexible built-in functions. The function will return if the object belongs to the given class or a subclass of the given class . Enter the following in the interactive shell:isinstance()
True
>>> 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
>>> isinstance(child, ChildClass) # 1
True
>>> isinstance(child, ParentClass) # 2
True
Note that the objects in isinstance()
the representation are instances of 1 as well as instances of 2. This makes sense because an object is a kind of object.child
ChildClass
ChildClass
ParentClass
ChildClass
ParentClass
You can also pass a tuple of class objects as the second argument to see if the first argument is any of the classes in the tuple:
>>> isinstance(42, (int, str, bool)) # True if 42 is an int, str, or bool.
True
A less commonly used issubclass()
built-in function recognizes whether the class object passed as the first argument is a subclass (or the same class) of the class object passed as 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
Just like with use isinstance()
, you can pass a tuple of class objects as the second argument issubclass()
to see if the first argument is a subclass of any of the classes in the tuple. isinstance()
The key difference between and issubclass()
is that issubclass()
two class objects are passed, whereas isinstance()
an object and a class object are passed.
class method
Class methods are associated with a class, not with a single object like regular methods. You can recognize a class method in your code when you see two tokens: the decorator def
before the method's statement and the use as first parameter, as shown in the example below.@classmethod
cls
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()
Parameters behave similarly , except that self
they refer to an object cls
instead of an object's class . This means that code within a class method cannot access individual object properties or call regular methods of the object. Class methods can only call other class methods or access class attributes. We use name because it is a Python keyword, and like other keywords, such as , or , we cannot use it for parameter names. We often call class attributes through class objects, as in . But we can also call them through any object of the class, just like in .cls
self
cls
class
if
while
import
ExampleClass.exampleClassMethod()
obj.exampleClassMethod()
Class methods are not commonly used. The most common use case is to provide __init__()
optional constructors other than . For example, what if the constructor could accept both a string of data required by the new object and a string of a filename containing the data required by the new object? We don't want __init__()
long and confusing parameter lists for methods. Instead, let's use a class method that returns a new object.
For example, let's create a AsciiArt
class. As you saw in Chapter 14, ASCII art uses text characters to form images.
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()
AsciiArt
class has a __init__()
method to which the image's text characters can be passed as a string. It also has a fromFile()
class method that can be passed the filename string of the text file containing the ASCII art. Both methods create AsciiArt
objects.
When you run this program and have a file containing ASCII art surfaces face.txt
, the output will look like this:
_______
| . . |
| \___/ |
|_______|
_______
| . . |
| \___/ |
|_______|
Class methods make your code easier to read than having __init__()
to do everything .fromFile()
Another benefit of class methods is that AsciiArt
subclasses of a class can inherit its fromFile()
method (and override it if necessary). That's why we call it in the method AsciiArt
of the class instead of . Calling also works in subclasses of , without modification, since the class is not hardcoded into the methods. But a call always calls the class's and not the subclass's . You can understand it as "an object representing this class"fromFile()
cls
(characters)
AsciiArt(characters)
cls()
AsciiArt
AsciiArt
AsciiArt()
AsciiArt
__init__()
__init__()
cls
Remember that just as regular methods should always use their parameters somewhere in the code self
, a class method should always use its cls
parameters. If your class method's code never uses cls
parameters, this is a sign that your class method is probably just a function.
class attribute
Class attributes are variables that belong to a class rather than an object. We create class attributes inside a class but outside all methods, just like we .py
create global variables inside a file but outside all functions. Here's an count
example of a class attribute named , 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.
CreateCounter
Classes have a count
class attribute named . All CreateCounter
objects share this property instead of having their own individual count
properties. That's why the lines in the constructor CreateCounter.count += 1
can record each object being created CreateCounter
. When you run the program, the output will look like this:
Objects created: 0
Objects created: 3
We rarely use class attributes. Even this "count how many objects have been created CreateCounter
" example can be done more simply by using a global variable instead of a class property.
static method
A static method does not have an self
or cls
parameter. Static methods are really just functions in that they cannot access properties or methods of the class or its objects. In Python, static methods are rarely needed. If you decide to use a function, then you should consider creating a regular function.
We define a static method by def
placing @staticmethod
a decorator before the static method's statement. Below 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 is little difference between static methods and functions in ExampleClassWithStaticMethod
a class . In fact, you might prefer to use a function, since you can call it without having to type the class name beforehand.sayHello()
sayHello()
Static methods are more common in other languages that don't have Python's flexible language features. Python includes static methods that mimic features of other languages, but provide little practical value.
When to Use Object-Oriented Classes and Static Attributes
You rarely need class methods, class properties and static methods. They are also prone to overuse. If you're thinking, "Why can't I use a function or a global variable instead?" that's a hint that you probably don't need to use class methods, class properties, or static methods. The only reason this intermediate-level book introduces them is so you can recognize them when you encounter them in code, but I discourage you from using them. They are useful if you are creating your own framework with a series of carefully crafted classes that are subclassed by programmers using the framework. But when you're writing simple Python applications, you probably don't need them.
For more discussion of these features and why you might or might not need them, read Phillip J. Eby's dirtsimple.org/2004/12/python-is-not-java.html
article "Python is not Java" and Ryan Tomayko tomayko.com/blog/2004/the-static-method-thing
's article "Static methods."
object oriented buzzword
Explanations of OOP usually start with a number of terms, 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've already covered inheritance, so I'll describe the other concepts here.
encapsulation
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 . That's basically what classes do: they group related properties and methods. For example, our WizCoin
class encapsulates three integers knuts
, sickles
and , galleons
into a single WizCoin
object.
The second definition is that encapsulation is an information hiding technique that lets an object hide complex implementation details about how the object works. You can see this in "Private Properties and Methods" on page 282, where BankAccount
objects provide deposit()
and withdraw()
methods to hide the details of how their _balance
properties are handled. Functions serve a similar black-box purpose: math.sqrt()
how the function computes the square root of a number is hidden. You just need to know that the function returns the square root of the number you pass it.
polymorphism
Polymorphism allows objects of one type to be treated as objects of another type. For example, len()
the function returns the length of the argument passed to it. You can pass a string len()
to see how many characters it has, but you can also pass a list or dictionary len()
to see how many entries or key-value pairs it has, respectively. This form of polymorphism is known as generic function or parametric polymorphism because it can handle many different types of objects.
Polymorphism also refers to ad hoc polymorphism or operator overloading , where operators (like +
or *
) can behave differently depending on the type of object they operate on. For example, +
the operator does mathematical addition when dealing with two integer values or floating-point values, but does string concatenation when dealing with two strings. Operator overloading is covered in Chapter 17.
When Not to Use Inheritance
It's easy to overengineer your classes using inheritance. As Luciano Ramaljo said, "Having things in order inspires our sense of order; programmers do it for fun." When several functions in a class or a module can achieve the same , we'll create classes, subclasses, and subclasses. But recall the Python mantra from Chapter 6: Simple is better than complex.
Using OOP allows you to organize your code into smaller units (classes in this case) that are py
easier to reason about than one large file containing 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 this case, it is beneficial to organize them into a class.
But here are some examples that don't require creating classes or using inheritance:
- If your class consists of methods that never use
self
orcls
parameters, delete the class and use functions instead of methods. - If you've created a parent class with only one subclass, but never created objects of the parent class, you can combine them into one class.
- If you create more than three or four levels of subclasses, you may be using inheritance unnecessarily. Merge these subclasses into fewer classes.
As the non-object-oriented and object-oriented versions of the tic-tac-toe program demonstrated in the previous chapter, it is of course possible to not use classes and still have a working, error-free program. Don't feel like you have to design your program into some complex network of classes. Simple solutions that work are better than complex solutions that don't work. Joel Spolsky writes in his blog post "Don't Let Astronaut Architects Scare You".
You should know how object-oriented concepts like inheritance work because they help you organize your code and make development and debugging easier. Because of Python's flexibility, the language not only provides OOP features, but also doesn't require you to use them when they don't fit your program's needs.
multiple inheritance
Many programming languages limit classes to at most one parent class. Python supports multiple parent classes by providing a feature called multiple inheritance . For example, we can have a class with flyInTheAir()
a method Airplane
and a class with floatOnWater()
a method Ship
. We can then create a class that inherits from Airplane
and Ship
by FlyingBoat
listing class
the two in the statement, separated by a comma. 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 objects we create FlyingBoat
will inherit 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 simple as long as the method names of the parent classes are different and do not overlap. These kinds of classes are called mixins . (That's just a generic term for a class; Python doesn't have mixin
keywords.) But what happens when we inherit from multiple complex classes that share method names?
MiniBoard
For example, consider the Tic Tac Toe board class from earlier in this chapter HintTTTBoard
. What if we wanted a class that displayed a miniature tic-tac-toe board and gave hints? Through multiple inheritance, we can reuse these existing classes. Add the following to tictactoe_oop.py
the end of your file, but before the statement that calls main()
the function if
:
class HybridBoard(HintBoard, MiniBoard):
pass
This class has nothing. It reuses code through inheritance HintBoard
and . MiniBoard
Next, change main()
the code in the function so that it creates an HybridBoard
object:
gameBoard = HybridBoard() # Create a TTT board object.
Both parent classes MiniBoard
and HintBoard
both have a getBoardStr()
method called , so HybridBoard
which one to inherit from? When you run this program, the output shows 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 magically merge MiniBoard
class getBoardStr()
methods and HintBoard
class getBoardStr()
methods to achieve both! But that's because I've written them to work with each other. In fact, if you change the order of the classes in HybridBoard
the class class
statement, it looks like this:
`class HybridBoard(MiniBoard, HintBoard):`
You completely lose the hint:
`--snip--`
X.. 123
.O. 456
X.. 789
To understand why this is the case, you need to understand Python's Method Resolution Order ( MRO ) and super()
how functions actually work.
method resolution order
Our tic-tac-toe program now has four classes to represent the board, three with defined getBoardStr()
methods and one with inherited getBoardStr()
methods, as shown in Figure 16-2.
Figure 16-2: The four classes in our tic-tac-toe program
When we call it on an HybridBoard
object getBoardStr()
, Python knows that HybridBoard
the class doesn't have a method with that name, so it checks its parent class. But the class has two parents, and they both have a getBoardStr()
method. Which will be called?
You can HybridBoard
find out by examining the MRO of a class, which is super()
an ordered list of classes that Python checks when inheriting a method or when a method calls a function. You can see the MRO of HybridBoard
a class by calling its method in an interactive shell :mro()
>>> from tictactoe_oop import *
>>> HybridBoard.mro()
[<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>]
As you can see from this return value, when HybridBoard
calling a method on , Python first HybridBoard
checks for it in the class. If not present, Python checks HintBoard
for class, then MiniBoard
class, and finally TTTBoard
class. At the end of each MRO list is the built-in object
class, which is the parent class of all classes in Python.
With single inheritance, determining the MRO is easy: just create a chain of parent classes. With multiple inheritance, it's trickier. Python's MRO follows the C3 algorithm, the details of which are beyond the scope of this book. But you can determine MRO by remembering two rules:
- Python checks subclasses before checking superclasses.
- Python examines
class
the inherited classes listed from left to right in the statement.
If we HybridBoard
call on an object getBoardStr()
, Python first checks HybridBoard
the class. Then, since the parent of the class is sum from left to HintBoard
right MiniBoard
, Python checks HintBoard
. This parent class has a getBoardStr()
method, so HybridBoard
inherit and call it.
But it doesn't end there: next, the method call super().getBoardStr()
. is a somewhat misleading name for a function in super
Python , since instead of returning the parent class, it returns the next class in the MRO. super()
This means that when we HybridBoard
call on an object getBoardStr()
, the next class in its MRO, HintBoard
after, is MiniBoard
, not the parent class TTTBoard
. So super().getBoardStr()
the call to the MiniBoard
class getBoardStr()
method that returns the miniature tic tac toe string. The rest of the code in the class after this super()
call appends the prompt text to this string.HintBoard
getBoardStr()
If we change the HybridBoard
class class
statement so that it is listed first MiniBoard
, HintBoard
its MRO will be MiniBoard
listed HintBoard
first. This means inheriting HybridBoard
from , without calling to. This ordering causes the miniature tic-tac-toe board to display silent errors: no calls , methods of classes never call methods of classes .MiniBoard
getBoardStr()
MiniBoard
super()
super()
MiniBoard
getBoardStr()
HintBoard
getBoardStr()
Multiple inheritance allows you to create a lot of functionality with a small amount of code, but can easily lead to over-engineered, hard-to-understand code. Supports single inheritance, mixin classes, or no inheritance. These techniques are often better able to accomplish the task of the program.
Summarize
Inheritance is a code reuse technique. It allows you to create subclasses that inherit the methods of their parent class. You can override these methods to give them new code, but you can also use super()
functions to call the original methods in the parent class. A subclass has an "is" relationship with its parent class because an object of the subclass is a type of object of the parent class.
In Python, the use of classes and inheritance is optional. Some programmers don't think the complexity of heavy use of inheritance is worth it. Using composition rather than inheritance is often more flexible because it implements a "has" relationship with objects 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 own objects of another class. For example, Customer
an object may have a property that is assigned to Date
the object birthdate
rather than to Customer
a subclass of the class Date
.
Just as type()
it can return the type of the object passed to it, isinstance()
and issubclass()
functions return the type and inheritance information of the object passed to them.
Classes can have object methods and properties, but they can also have class methods, class properties, and static methods. Although these are rarely used, they can support other object-oriented techniques that global variables and functions cannot.
Python allows classes to inherit from multiple parent classes, although this can lead to difficult-to-understand code. super()
Functions and methods of a class determine how methods are inherited based on the MRO. You can mro()
view a class's MRO in the interactive shell by invoking the class's methods.
This chapter and the previous one cover general OOP concepts. In the next chapter, we'll explore Python-specific OOP techniques. `