Mastering Objectoriented Python
上QQ阅读APP看书,第一时间看更新

Hard totals, soft totals, and polymorphism

Let's define Hand so that it will perform a meaningful mixed-class comparison. As with other comparisons, we have to determine precisely what we're going to compare.

For equality comparisons between Hands, we should compare all cards.

For ordering comparisons between Hands, we need to compare an attribute of each Hand object. For comparisons against an int literal, we should compare the Hand object's total against the literal. In order to have a total, we have to sort out the subtlety of hard totals and soft totals in the game of Blackjack.

When there's an ace in a hand, then the following are two candidate totals:

  • The soft total treats an ace as 11. If the soft total is over 21, then this version of the ace has to be ignored.
  • The hard total treats an ace as 1.

This means that the hand's total isn't a simple sum of the cards.

We have to determine if there's an ace in the hand first. Given that information, we can determine if there's a valid (less than or equal to 21) soft total. Otherwise, we'll fall back on the hard total.

One symptom of Pretty Poor Polymorphism is relying on isinstance() to determine the subclass membership. Generally, this is a violation of the basic encapsulation. A good set of polymorphic subclass definitions should be completely equivalent with the same method signatures. Ideally, the class definitions are opaque; we don't need to look inside the class definition. A poor set of polymorphic classes uses extensive isinstance() testing. In some cases, isinstance() is necessary. This can arise when using a built-in class. We can't retroactively add method functions to built-in classes, and it might not be worth the effort of subclassing them to add a polymorphism helper method.

In some of the special methods, it's necessary to see isinstance() used to implement operations that work across multiple classes of objects where there's no simple inheritance hierarchy. We'll show you an idiomatic use of isinstance() for unrelated classes in the next section.

For our cards class hierarchy, we want a method (or an attribute) that identifies an ace without having to use isinstance(). This is a polymorphism helper method. It ensures we can tell otherwise equivalent classes apart.

We have two choices:

  • Add a class-level attribute
  • Add a method

Because of the way the insurance bet works, we have two reasons to check for aces. If the dealer's card is an ace, it triggers an insurance bet. If the dealer's hand (or the player's hand) has an ace, there will be a soft total versus hard total calculation.

The hard total and soft total always differ by the card.soft–card.hard value for the card that's an ace. We can look inside the definition of AceCard to see that this value is 10. However, looking at the implementation breaks encapsulation by looking deeply at a class implementation.

We can treat BlackjackCard as opaque and check to see whether card.soft-card.hard!=0 is true. If this is true, it is sufficient information to work out the hard total versus soft total of the hand.

The following is a version of the total method that makes use of the soft versus hard delta value:

def total( self ):
    delta_soft = max( c.soft-c.hard for c in self.cards )
    hard = sum( c.hard for c in self.cards )
    if hard+delta_soft <= 21: return hard+delta_soft
    return hard

We'll compute the largest difference between the hard and soft total as delta_soft. For most cards, the difference is zero. For an ace, the difference will be nonzero.

Given the hard total and delta_soft, we can determine which total to return. If hard+delta_soft is less than or equal to 21, the value is the soft total. If the soft total is greater than 21, then revert to a hard total.

We can consider making the value 21 a manifest constant in the class. A meaningful name is sometimes more helpful than a literal. Because of the rules of Blackjack, it's unlikely that 21 would ever change to a different value. It's difficult to find a more meaningful name than the literal 21.