16
Object-Oriented Programming and Inheritance

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.

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:

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 ChildClasssubclassesParentClass, meaning that ChildClass will have all the same methods as ParentClass. We say that ChildClassinherits 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.

f16001

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

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 VehicleFourWheelVehicleCar, AnimalBirdSparrow, or ShapeRectangleSquare. 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.

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 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.

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 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 += '
X can win in one more move.'
        if oCanWin:
            boardStr += '
O 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.

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 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:

  • 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 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 WizCoin1 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.

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 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.

The isinstance() and issubclass() Functions

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

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(' _______
' +
                 '|  . .  |
' +
                 '| \___/ |
' +
                 '|_______|')
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.

Class Attributes

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.

Static Methods

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.

When to Use Class and Static Object-Oriented Features

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.

Object-Oriented Buzzwords

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.

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 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

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.

When Not to Use Inheritance

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:

  • If your class consists of methods that never use the self or cls parameter, delete the class and use functions in place of the methods.
  • If you’ve created a parent with only a single child class but never create objects of the parent class, you can combine them into a single class.
  • If you create more than three or four levels of subclasses, you’re probably using inheritance unnecessarily. Combine those subclasses into fewer classes.

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.

Multiple Inheritance

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.

Method Resolution Order

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.

f16002

Figure 16-2: The four classes in our tic-tac-toe board program

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:

  • Python checks child classes before parent classes.
  • Python checks inherited classes listed left to right in the 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.

Summary

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 method 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.228.78