Chapter 18 · pp. 193–204
Inheritance
Inheritance lets you build a new class by modifying an existing one instead of starting from scratch. This chapter uses playing cards, decks and hands to show how it works. Along the way you will meet class attributes, operator overloading with __lt__, class diagrams, and a design plan called data encapsulation. (p. 193)
Representing cards: encoding
A playing card has two attributes, a suit and a rank. The harder question is what type each should be. Strings such as 'Queen' are readable, but they make it hard to ask which card is higher. (p. 193)
The alternative is to encode suits and ranks as integers. Encoding means representing one set of values with another by building a mapping between them. It is not meant to hide anything. Hiding information is encryption, which is a different idea. (pp. 193–194)
The suit codes are:
- Clubs → 0
- Diamonds → 1
- Hearts → 2
- Spades → 3
Numbered ranks map to their own numbers. Jack is 11, Queen is 12 and King is 13. These mappings are part of the program's design, not something written in the code. Because higher suits get higher numbers, comparing two codes compares the suits. (p. 194)
The Card class stores the two values in its __init__ method:
class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
Both parameters are optional. Card() gives the default card, the 2 of Clubs. Card(1, 12) gives suit 1 (Diamonds) and rank 12 (Queen), so it is the Queen of Diamonds. (p. 194)
Class attributes and instance attributes
To print a card in words, we need to turn the codes back into names. Two lists of strings do this, suit_names and rank_names. They are written inside the class but outside any method, which makes them class attributes. They belong to the class object Card, and only one copy exists. (pp. 194–195)
suit and rank are instance attributes. Each card has its own pair of values. Both kinds are reached with dot notation, such as Card.rank_names for a class attribute or self.rank for an instance attribute. (pp. 194–195)
The __str__ method uses both kinds together. The expression Card.rank_names[self.rank] works in two steps:
- Python reads
self.rank from the object. For Card(2, 11) this is 11.
- Python uses 11 as an index into the list
rank_names on the class. The result is 'Jack'.
__str__ does the same lookup in suit_names. For Card(2, 11) it prints 'Jack of Hearts'. (pp. 194–195)
The first item of rank_names is None. No card has rank 0, so this placeholder keeps every index equal to its rank. Index 2 gives '2', index 3 gives '3', and so on. A dictionary would avoid the placeholder. (p. 195)
Comparing cards with __lt__
For built-in types, operators like < just work. For your own classes, you can define how < behaves by writing a method named __lt__ ('less than'). It takes self and other and returns True if self is strictly less than other. Python calls it when you write card1 < card2. (p. 195)
First you must decide what 'less' means. Is the 3 of Clubs below the 2 of Diamonds? The 3 has the higher rank, but Diamonds is the higher suit. The right answer depends on the game. The chapter chooses suit as more important, so the 2 of Diamonds wins. (pp. 195–196)
The plan is to compare suits first. Only if the suits are equal do you compare ranks:
def __lt__(self, other):
if self.suit < other.suit: return True
if self.suit > other.suit: return False
return self.rank < other.rank
A shorter version compares tuples. Python compares tuples element by element, so (self.suit, self.rank) < (other.suit, other.rank) gives the same result. (p. 196)
The same __lt__ method is what list.sort uses to order objects. A Deck.sort method can simply call self.cards.sort(). (p. 198)
Decks
A Deck keeps its cards in a list attribute called cards. Its __init__ builds all 52 cards with a nested loop. The outer loop runs suit over range(4). The inner loop runs rank over range(1, 14). Each pass creates Card(suit, rank) and appends it to self.cards. The total is 4×13=52 cards. (p. 196)
The Deck.__str__ method makes a list of strings, one per card, by calling str(card). That call runs Card.__str__. The method then returns '\n'.join(res). Building a list and joining it once is an efficient way to make a large string. The printout looks like 52 lines, but it is one string with newline characters inside. (pp. 196–197)
Deck operations and veneers
pop_card removes and returns the last card using the list method pop. add_card appends a card. shuffle calls random.shuffle on the list, so you must import random. (p. 197)
Methods like these do almost no work themselves. They only present a list operation in deck terms. Such a method is called a veneer. add_card is a good example. It simply calls self.cards.append(card), but the name fits the way a deck is used. (p. 197)
Inheritance: parents, children and overriding
Inheritance is the ability to define a new class that is a modified version of an existing class. A hand of cards is a good case. Like a deck, it is a collection of cards that needs adding and removing. But it also needs things a deck does not, such as comparing poker hands. (p. 198)
You write the parent's name in parentheses after the new class name:
class Hand(Deck):
...
Deck is the parent and Hand is the child. The child gets pop_card and add_card without rewriting them. (p. 198)
The child also inherits __init__, which is a problem. Deck's version would put 52 cards into every hand. To fix this, Hand defines its own __init__ with the same name. This overrides the parent's method. It sets self.cards = [] and stores a label. When you create a Hand, Python runs the child's version. (pp. 198–199)
A method written once in Deck can serve several classes. For example, move_cards(self, hand, num) deals cards by calling hand.add_card(self.pop_card()) in a loop. It works for Deck to Hand, Hand to Hand, or Hand to Deck. (p. 199)
Benefits and costs. Inheritance helps you reuse code, because you can customize a parent's behavior without changing the parent. It can also match the real structure of the problem. The drawback is readability. When a method is called, it may be unclear where its definition lives, because it may be spread across several classes or modules. Many tasks can be done just as well without inheritance. (p. 199)
Class diagrams
A class diagram shows the classes in a program and how they relate. It is more abstract than an object diagram, which is a snapshot of one moment. There are three kinds of relationship. (pp. 199–200)
- HAS-A: objects of one class hold references to objects of another. A Deck has Cards. It is drawn with a standard arrow.
- IS-A: one class inherits from another. A Hand is a kind of Deck. It is drawn with a hollow triangle arrowhead.
- Dependency: one class uses another, for example as a parameter, but does not store it. It is drawn with a dashed arrow.
A multiplicity next to a HAS-A arrow says how many objects are referenced. It can be a number such as 52, a range such as 5..7, or a star for any number. Built-in types like list and dict are usually left out of these diagrams. (p. 200)
Data encapsulation as a design plan
Sometimes it is not obvious which objects a program needs. In that case you can start with global variables and functions and reorganize later. This plan is called data encapsulation. (p. 201)
The Markov text example uses two globals, suffix_map and prefix. Because they are global, only one analysis can run at a time. A second text would mix its data into the first. The fix is a Markov class whose __init__ sets self.suffix_map = {} and self.prefix = (). The functions that use these variables, such as process_word, become methods that read self.prefix and self.suffix_map. Each analysis now has its own object. (pp. 201–202)
The steps are:
- Write functions that use global variables, and get the program working.
- Look for which globals go with which functions.
- Make the related variables attributes of an object.
- Turn the related functions into methods of the new class.
Changing the design without changing the behavior is called refactoring. (p. 202)
Check yourself
- Why are integer codes better than strings for comparing cards? What does
Card(1, 12) represent?
- What is the difference between a class attribute and an instance attribute? Give one example of each from
Card.
- Why does
rank_names start with None, and how could you avoid that?
Hand inherits from Deck but defines its own __init__. Why, and what is this called?
- A program has global variables
counts and total used by three functions. Describe how you would use data encapsulation to redesign it.