Inheritance is especially useful when you want to create a more specialized version of an existing class. As you just learned, by inheriting from an existing class, a new class gets all of the methods and attributes of the existing class. But you can also add methods and attributes to the new class to extend what objects of the new class can do.
For example, imagine that your Drag_Racer defines a drag racer with methods stop() and go(). You could create a new class for a specialized type of drag racer that can clean its windshield (you get a lot of squashed bugs at 250 miles per hour) by basing it on the existing Drag_Racer class. Your new class would automatically inherit stop() and go() from Drag_Racer. So, all you'd have to do is define one new method for cleaning the windshield and the new class would be done.
The Playing Cards 2.0 program is based on the Playing Cards program. The new version introduces the Deck class to describe a deck of playing cards. However, unlike any other class you've seen, Deck is based on an existing class, Hand. As a result, Deck automatically inherits all of Hand's methods. I create Deck this way because a deck of cards is really like a specialized hand of cards. It's a hand, but with extra behaviors. A deck can do anything that a hand can. It's a collection of cards. It can give a card to another hand, and so on. On top of that, a deck can do a few things that a hand can't. A deck can be shuffled and it can deal cards to multiple hands. The Playing Cards 2.0 program creates a deck that deals cards to two different hands. Figure 9.5 illustrates the results of the program.
Figure 9.5: The Deck object inherits all of the methods of the Hand class.
I begin the new program like the old version. The first two classes, Card and Hand, are the same as before:
# Playing Cards 2.0 # Demonstrates inheritance - object extension # Michael Dawson 4/9/03 class Card(object): """ A playing card. """ RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"] SUITS = ["c", "d", "h", "s"] def __init__(self, rank, suit): self.rank = rank self.suit = suit def __str__(self): rep = self.rank + self.suit return rep class Hand(object): """ A hand of playing cards. """ def __init__(self): self.cards = [] def __str__(self): if self.cards: rep = "" for card in self.cards: rep += str(card) + "\t" else: rep = "<empty>" return rep def clear(self): self.cards = [] def add(self, card): self.cards.append(card) def give(self, card, other_hand): self.cards.remove(card) other_hand.add(card)
The next thing I do is create the Deck class. You can see from the class header that Deck is based on Hand:
class Deck(Hand):
Hand is called a base class because Deck is based on it. Deck is considered a derived class because it derives part of its definition from Hand. As a result of this relationship, Deck inherits all of Hand's methods. So, even if I didn't define a single new method in this class, Deck objects would still have all of the methods defined in Hand:
__init__()
__str__()
clear()
add()
give()
If it helps, for this simple example, you can even imagine that you've copied and pasted all of Hand's methods right into Deck because of inheritance.
You can extend a derived class by defining additional methods in it. That's what I do in the class definition of Deck:
""" A deck of playing cards. """ def populate(self): for suit in Card.SUITS: for rank in Card.RANKS: self.add(Card(rank, suit)) def shuffle(self): import random random.shuffle(self.cards) def deal(self, hands, per_hand = 1): for rounds in range(per_hand): for hand in hands: if self.cards: top_card = self.cards[0] self.give(top_card, hand) else: print "Can't continue deal. Out of cards!"
So, in addition to all of the methods that Deck inherits, it has the following new methods:
populate()
shuffle()
deal()
As far as client code is concerned, any Deck method is as valid as any other—whether it's inherited from Hand or defined in Deck. And all of a Deck object's methods are invoked the same way, through dot notation.
The first thing I do in the main part of the program is instantiate a new Deck object:
# main deck1 = Deck()
Looking at the class, you'll notice that I don't define a constructor method in Deck. But Deck inherits the Hand constructor, so that method is automatically invoked with the newly created Deck object. As a result, the new Deck object gets a cards attribute which is initialized to an empty list, just as any newly created Hand object would get a similar cards attribute. Finally, the assignment statement assigns the new object to deck1.
Now armed with a new (but empty) deck, I print it:
print "\nNew deck:" print deck1
I didn't define the special __str__() method in Deck either, but again, Deck inherits the method from Hand. Since the deck is empty, the code displays the text <empty>. So far, a deck seems just like a hand. That's because a deck is a specialized type of hand. Remember, a deck can do anything a hand can, plus more.
An empty deck is no fun, so I invoke the object's populate() method, which populates the deck with the traditional 52 cards:
deck1.populate()
Now the deck has finally done something a hand can't. That's because the populate() method is a new method that I define in the Deck class. The populate() method loops through all of the 52 possible combinations of values of Card.SUITS and Card.RANKS (one for each card in a real deck). For each combination, the method creates a new Card object that it adds to the deck.
Next, I print the deck:
print "\nPopulated deck:" print deck1
This time, all 52 cards are displayed! But if you look closely, you'll see that they're in an obvious order. To make things interesting, I shuffle the deck:
deck1.shuffle()
I define the shuffle() method in Deck. It imports the random module and then calls the random.shuffle() function with the object's cards attribute. As you might guess, the random.shuffle() method shuffles a list's elements into a random order. So, all of the elements of cards get shuffled. Perfect.
Now, with the cards in random order, I display the deck again:
print "\nShuffled deck:" print deck1
Next, I create two Hand objects and put them in a list that I assign to hands:
my_hand = Hand() your_hand = Hand() hands = [my_hand, your_hand]
Then, I deal each hand five cards:
deck1.deal(hands, per_hand = 5)
The deal() method is a new method I define in Deck. It takes two arguments: a list of hands and the number of cards to deal each hand. The method gives a card from the deck to each hand. If the deck is out of cards, the method prints the message "Can't continue deal. Out of cards!" The method repeats this process for the number of cards to be dealt each hand. So, this line deals five cards from deck1 to each hand (my_hand and your_hand).
To see the results of the deal, I print each hand and the deck once more:
print "\nDealt 5 cards to my hand and your hand." print "My hand:" print my_hand print "Your hand:" print your_hand print "Deck:" print deck1
By looking at the output, you can see that each hand has 5 cards and the deck now has only 42.
Finally, I put the deck back to its initial state by clearing it:
deck1.clear() print "\nCleared the deck."
And then I print the deck one last time:
print "Deck:", deck1