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
Card
s. The __init__
method
function of Deck
creates appropriate
Card
s 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 Card
s 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 Card
s 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 Card
s 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 Card
s,
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.