One of the four important features of class definition is
inheritance. You can create a subclass which inherits all of the
features of a superclass. The subclass can add or replace method
functions of the superclass. This is typically used by defining a
general-purpose superclass and creating specialized subclasses that all
inherit the general-purpose features but add special-purposes features
of their own.
We do this by specifying the parent class when we create a
subclass.
class
subclass
(
superclass
):
suite
All of the methods of the superclass are, by definition, also part
of the subclass. Often the suite of method functions will add to or
override the definition of a parent method.
If we omit providing a superclass, we create a
classical class definition, where the Python type
is instance; we have to do additional processing
to determine the actual type. When we use object
as the superclass, the Python type is reported more simply as the
appropriate class object. As a general principle,
every class definition should be a subclass of
object, either directly or indirectly.
Extending a Class. There are two trivial subclassing techniques. One defines a
subclass which adds new methods to the superclass. The other overrides
a superclass method. The overriding technique leads to two classes
which are polymorphic because they have the same interface. We'll
return to polymorphism in the section called “Polymorphism”.
Here's a revised version of our basic Dice
class and a subclass to create CrapsDice.
Example 22.1. crapsdice.py
#!/usr/bin/env python
"""Define a Die, Dice and CrapsDice."""
class Die( object ):
See the definition in Example 21.1, “die.py”.
class Dice( object ):
"""Simulate a pair of dice."""
def __init__( self ):
"Create the two Die objects."
self.myDice = ( Die(), Die() )
def roll( self ):
"Return a random roll of the dice."
for d in self.myDice:
d.roll()
def getTotal( self ):
"Return the total of two dice."
return self.myDice[0].value + self.myDice[1].value
def getTuple( self ):
"Return a tuple of the dice."
return self.myDice
class CrapsDice( Dice ):
"""Extends Dice to add features specific to Craps."""
def hardways( self ):
"""Returns True if this was a hardways roll?"""
return self.myDice[0].value == self.myDice[1].value
def isPoint( self, value ):
"""Returns True if this roll has the given total"""
return self.getTotal() == value
The CrapsDice class contains all the
features of Dice as well as the additional
features we added in the class declaration. We can write applications
which create a CrapsDice instance. We can, for example, evaluate the
roll and hardways
methods of CrapsDice. The
roll method is inherited from
Dice, but the hardways
method is a direct part of CrapsDice.
Adding Instance Variables. Adding new instance variables requires that we extend the
__init__ method. In this case we want an
__init__ function that starts out doing
everything the superclass __init__ function does,
and then creates a few more attributes. Python provides us the
super function to help us do this. We can use
super to distinguish between method functions
with the same name defined in the superclass and extended in a
subclass.
-
super(
type
,
variable
)
-
This will do two things: locate the superclass of the given
type, and it will assure that the given variable is an appropriate
object of the superclass. This is often used to call a superclass
method from within a subclass:
super(
MyClass
,self).
aMethod
(
args
).
Here's a template that shows how a subclass
__init__ method uses super
to evaluate the superclass __init__
method.
class Subclass( Superclass ):
def __init__( self ):
super(Subclass,self)__init__()
Subclass-specific stuff
This will provide the original self
variable to parent class method function so that it gets the superclass
initialization. After that, we can add our subclass
initialization.
We'll look at additional techniques for creating very flexible
__init__ methods in the section called “Initializer Techniques”.
Various Kinds of Cards. Let's look clisely at the problem of cards in Blackjack. All
cards have several general features: they have a rank and a suit. All
cards have a point value. However, some cards use their rank for point
value, other cards use 10 for their point value and the aces can be
either 1 or 11, depending on the the rest of the cards in the hand. We
looked at this in the the section called “Playing Cards and Decks”
exercise in Chapter 21, Classes
.
We can model this very accurately by creating a
Card class that encapsulates the generic features
of rank, suit and point value. Our class will have instance variables
for these attribites. The class will also have two functions to return
the hard value and soft value of this card. In the case of ordinary
non-face, non-ace cards, the point value is always the rank. We can use
this Card class for the number cards, which are
most common.
class Card( object ):
"""A standard playing card for Blackjack."""
def __init__( self, r, s ):
self.rank, self.suit = r, s
self.pval= r
def __str__( self ):
return "%2d%s" % ( self.rank, self.suit )
def getHardValue( self ):
return self.pval
def getSoftValue( self ):
return self.pval
We can create a subclass of Card which is
specialized to handle the face cards. This subclass simply overrides the
value of self.pval, using 10 instead of the rank
value. In this case we want a
FaceCard.__init__ method
that uses the parent's
Card.__init__ method, and
then does additional processing. The existing definitions of
getHardValue and getSoftValue
method functions, however, work fine for this subclass. Since
Card is a subclass of
object, so is
FaceCard.
Additionally, we'd like to report the card ranks using letters (J,
Q, K) instead of numbers. We can override the
__str__ method function to do this translation
from rank to label.
class FaceCard( Card ):
"""A 10-point face card: J, Q, K."""
def __init__( self, r, s ):
super(FaceCard,self).__init__( r, s )
self.pval= 10
def __str__( self ):
label= ("J","Q","K")[self.rank-11]
return "%2s%s" % ( label, self.suit )
We can also create a subclass of Card for
Aces. This subclass inherits the parent class
__init__ function, since the work done there is
suitable for aces. The Ace class, however,
provides a more complex algorithms for the
getHardValue and getSoftValue
method functions. The hard value is 1, the soft value is 11.
class Ace( Card ):
"""An Ace: either 1 or 11 points."""
def __str__( self ):
return "%2s%s" % ( "A", self.suit )
def getHardValue( self ):
return 1
def getSoftValue( self ):
return 11
Deck and Shoe as Collections of Cards. In a casino, we can see cards handled in a number of different
kinds of collections. Dealers will work with a single deck of 52 cards
or a multi-deck container called a shoe. We can also see the dealer
putting cards on the table for the various player's hands, as well as
a dealer's hand.
Each of these collections has some common features, but each also
has unique features. Sometimes it's difficult to reason about the
various classes and discern the common features. In these cases, it's
easier to define a few classes and then refactor the common features to
create a superclass with elements that have been removed from the
subclasses. We'll do that with Decks and Shoes.
We can define a Deck as a sequence of
Cards. The __init__ method
function of Deck creates appropriate
Cards of each subclass;
Card objects in the range 2 to 10,
FaceCard obejcts with ranks of 11 to 13, and
Ace objects with a rank of 1.
class Deck( object ):
"""A deck of cards."""
def __init__( self ):
self.cards= []
for suit in ( "C", "D", "H", "S" ):
self.cards+= [Card(r,suit) for r in range(2,11)]
self.cards+= [TenCard(r,suit) for r in range(11,14)]
self.cards+= [Ace(1,suit)]
def deal( self ):
for c in self.cards:
yield c
In this example, we created a single instance variable
self.cards within each Deck
instance. For dealing cards, we've provided a generator function which
yields the Cards in a random order. We've omitted
the randomization from the deal function; we'll
return to it in the exercises.
For each suit, we created the Cards of that
suit in three steps.
-
We created the number cards with a list comprehension to
generate all ranks in the range 2 through 10.
-
We created the face cards with a similar process, except we
use the TenCard class constructor, since
blackjack face cards all count as having ten points.
-
Finally, we created a singleton list of an
Ace instance for the given suit.
We can use Deck objects to create an
multi-deck shoe; a shoe is what dealers use in
casinos to handle several decks of slippery playing cards. The
Shoe class will create six separate decks, and
then merge all 312 cards into a single sequence.
class Shoe( object ):
"""Model a multi-deck shoe of cards."""
def __init__( self, decks=6 ):
self.cards= []
for i in range(decks):
d= Deck()
self.cards += d.cards
def deal( self ):
for c in self.cards:
yield c
For dealing cards, we've provided a generator function which
yields the Cards in a random order. We've omitted
the randomization from the deal function; we'll
return to it in the exercises.
Factoring Out Common Features. When we compare Deck and Shoe, we see two obviously common
features: they both have a collection of Cards,
called self.cards; they both have a
deal method which yields the set of
cards.
We also see things which are different. The most obvious
differences are details of initializing self.cards.
It turns out that the usual procedure for dealing from a shoe involves
shuffling all of the cards, but dealing from only four or five of the
six available decks. This is done by inserting a marker one or two decks
in from the end of the shoe.
In factoring out the common features, we have a number of
strategies.
-
One of our existing classes is already generic-enough to be
the superclass. In the Card example, we used
the generic Card class as superclass for
other cards as well as the class used to implement the number cards.
In this case we will make concrete object instances from the
superclass.
-
We may need to create a superclass out of our subclasses.
Often, the superclass isn't useful by itself; only the subclasses
are really suitable for making concrete object instances. In this
case, the superclass is really just an abstraction, it isn't meant
to be used by itself.
Here's an abstract CardDealer from which we
can subclass Deck and
Shoe. Note that it does not create any cards.
Each subclass must do that. Similarly, it can't deal properly because it
doesn't have a proper shuffle method
defined.
class CardDealer( object ):
def __init__( self ):
self.cards= []
def deal( self ):
for c in self.shuffle():
yield c
def shuffle( self ):
return NotImplemented
Python does not have a formal notation for abstract or concrete
superclasses. When creating an abstract superclass it is common to
return NotImplemented or raise
NotImplementedError to indicate that a
method must be overridden by a subclass.
We can now rewrite Deck as subclasses of
CardDealer.
class Deck( CardDealer ):
def __init__( self ):
super(Deck,self).__init__()
for s in ("C","D","H","S"):
Build the three varieties of cards
def shuffle( self ):
Randomize all cards, return all cards
We can also rewrite Shoe as subclasses of
CardDealer.
class Shoe( CardDealer ):
def __init__( self, decks=6 ):
CardDealer.__init__( self )
for i in range(decks):
d= Deck()
self.cards += d.cards
def shuffle( self ):
Randomize all cards, return a subset of the cards
The benefit of this is to assure that Deck
and Shoe actually share common features. This is
not "cut and paste" sharing. This is "by definition" sharing. A change
to CardDealer will change both Deck and Shoe,
assuring complete consistency.