One of the four important features of class definition is
polymorphism. Polymorphism exists when you define a number of subclasses
which have commonly named methods. A function can use objects of any of
the polymorphic classes without being aware that the classes are
distinct.
In some languages, it is essential that the polymorphic classes
have the same interface (or be subinterfaces of a common parent
interface), or be subclasses of a common superclass. This is sometimes
called "strong, hierarchical typing", since the type rules are very
rigid and follow the subclass/subinterface hierarchy.
Python implements something that is less rigid, often called "duck
typing". The phrase follows from a quote attributed to James Whitcomb
Riley: "When I see a bird that walks like a duck and swims like a duck
and quacks like a duck, I call that bird a duck." In short, two objects
are effectively of the class Duck
if they have a
common collection of methods (walk, swim and quack, for example.)
When we look at the examples for Card
,
FaceCard
, Ace
in the section called “Inheritance”, we see that all three classes
have the same method names, but have different implementations for some
of these methods. These three classes are polymorphic. A client class
like Hand
can contain individual objects of any
of the subclasses of Card
. A function can
evaluate these polymorphic methods without knowing which specific
subclass is being invoked.
In our example, both FaceCard
and
Ace
were subclasses of
Card
. This subclass relationship isn't necesary
for polymorphism to work correctly in Python. However, the subclass
relationship is often an essential ingredient in an overall design that
relies on polymorphic classes.
What's the Benefit? If we treat all of the various subclasses of
Card
in a uniform way, we effectively delegate
any special-case processing into the relevant subclass. We concentrate
the implementation of a special case in exactly one place.
The alternative is to include
if
-statements all
over our program to enforce special-case processing rules. This
diffusing of special-case processing means that many components wind up
with an implicit relationship. For example, all portions of a program
that deal with Card
s would need multiple
if
-statements to separate the number card points,
face card points and ace points.
By making our design polymorphic, all of our subclasses of
Card
have ranks and suits, as well as hard and
soft point values. We we can design the Deck
and
Shoe
classes to deal cards in a uniform way. We
can also design a Hand
class to total points
without knowing which specific class to which a
Card
object belongs.
Similarly, we made our design for Deck
and
Shoe
classes polymorphic. This allows us to model
one-deck blackjack or multi-deck blackjack with no other changes to our
application.
The Hand of Cards. In order to completely model Blackjack, we'll need a class for
keeping the player and dealer's hands. There are some differences
between the two hands: the dealer, for example, only reveals their
first card, and the dealer can't split. There are, however, some
important similarities. Every kind of Hand
must
determine the hard and soft point totals of the cards.
The hard point total for a hand is simply the hard total of all
the cards. The soft total, on the other hand, is not simply the soft
total of all cards. Only the Ace
cards have
different soft totals, and only one Ace can meaningfully contribute it's
soft total of 11. Generally, all cards provide the same hard and soft
point contributions. Of the cards where the hard and soft values differ,
only one such card needs to be considered.
Note that we are using the values of the
getHardValue
and getSoftValue
methods. Since this test applies to all classes of cards, we preserve
polymorphism by checking this property of every card. We'll preserving
just one of the cards with a soft value that is different from the hard
value. At no time do use investigate the class of a
Card
to determine if the card is of the class
Ace
. Examining the class of each object
needlessly constrains our algorithm. Using the polymorphic methods means
that we can make changes to the class structure without breaking the
processing of the Hand
class.
Pretty Poor Polymorphism
The most common indicator of poor use polymorphism is using the
type
, isinstance
and
issubclass
functions to determine the class of an
object. These should used rarely, if at all. All processing should be
focused on what is different about the objects, not the class to which
an object belongs.
We have a number of ways to represent the presence of a
Card
with a distinct hard and soft value.
-
An attribute with the point difference (usually 10).
-
A collection of all Card
s except for
one Card
with a point difference, and a
single attribute for the extra card.
We'll choose the first implementation. We can use use a sequence
to hold the cards. When cards are added to the hand, the first card that
returns distinct values for the hard value and soft value will be used
to set a variable has keeps the hard vs. soft point difference.
Example 22.2. hand.py
class Hand( object ):
"""Model a player's hand."""
def __init__( self ):
self.cards = [ ]
self.softDiff= 0
def addCard( self, aCard ):
self.cards.append( aCard )
if aCard.getHardValue() != aCard.getSoftValue():
if self.softDiff == 0:
self.softDiff= aCard.getSoftValue()-aCard.getHardValue()
def points( self ):
"""Compute the total points of cards held."""
p= 0
for c in self.cards:
p += c.getHardValue()
if p + self.softDiff <= 21:
return p + self.softDiff
else:
return p
|
The __init__ special function creates
the instance variable, self.cards , which we
will use to accumulate the Card objects
that comprise the hand. This also sets
self.softDiff which is the difference in
points between hard and soft hands. Until we have an Ace, the
difference is zero. When we get an Ace, the difference will be
10.
|
|
We provide an addCard method that
places an additional card into the hand. At this time, we
examine the Card to see if the soft value
is different from the hard value. If so, and we have not set the
self.softDiff yet, we save this
difference.
|
|
The points method evaluates the hand.
It initializes the point count, p , to zero.
We start a
for
-loop to assign each card
object to c . We could, as an alternative, use
a reduce function to perform this
operation: reduce( lambda a,b:a+b.getSoftValue(),
self.cards, 0 ) .
If the total with the self.softDiff is
21 or under, we have a soft hand, and these are the total
points. If the total with the self.softDiff
is over 21, we have a hard hand. The hard hand may total more
than 21, in which case, the hand is bust.
|