If we want to perform a statistical analysis of specific Hand instances, we might want to create a dictionary that maps a Hand instance to a count. We can't use a mutable Hand class as the key in a mapping. We can, however, parallel the design of set and frozenset and create two classes: Hand and FrozenHand. This allows us to freeze a Hand instance by creating FrozenHand; the frozen version is immutable and can be used as a key in a dictionary.
The following is a simple Hand definition:
class Hand:
def __init__(self, dealer_card: Card2, *cards: Card2) -> None:
self.dealer_card = dealer_card
self.cards = list(cards)
def __str__(self) -> str:
return ", ".join(map(str, self.cards))
def __repr__(self) -> str:
cards_text = ", ".join(map(repr, self.cards))
return f"{self.__class__.__name__}({self.dealer_card!r}, {cards_text})"
def __format__(self, spec: str) -> str:
if spec == "":
return str(self)
return ", ".join(f"{c:{spec}}" for c in self.cards)
def __eq__(self, other: Any) -> bool:
if isinstance(other, int):
return self.total() == cast(int, other)
try:
return (
self.cards == cast(Hand, other).cards
and self.dealer_card == cast(Hand, other).dealer_card
)
except AttributeError:
return NotImplemented
This is a mutable object; it does not compute a hash value, and can't be used in a set or dictionary key. It does have a proper equality test that compares two hands. As with previous examples, the parameter to the __eq__() method has a type hint of Any, and a do-nothing cast() function is used to tell the mypy program that the argument values will always be instances of Hand. The following is a frozen version of Hand:
import sys
class FrozenHand(Hand):
def __init__(self, *args, **kw) -> None:
if len(args) == 1 and isinstance(args[0], Hand):
# Clone a hand
other = cast(Hand, args[0])
self.dealer_card = other.dealer_card
self.cards = other.cards
else:
# Build a fresh Hand from Card instances.
super().__init__(*args, **kw)
def __hash__(self) -> int:
return sum(hash(c) for c in self.cards) % sys.hash_info.modulus
The frozen version has a constructor that will build one Hand class from another Hand class. It defines a __hash__() method that sums the card's hash value, which is limited to the sys.hash_info.modulus value. For the most part, this kind of modulus-based calculation works out well for computing the hashes of composite objects. We can now use these classes for operations such as the following code snippet:
from collections import defaultdict
stats = defaultdict(int) d = Deck() h = Hand(d.pop(), d.pop(), d.pop()) h_f = FrozenHand(h) stats[h_f] += 1
We've initialized a statistics dictionary, stats, as a defaultdict dictionary that can collect integer counts. We could also use a collections.Counter object for this.
By freezing an instance of the Hand class, we can compute a hash and use it as a key in a dictionary. This makes it easy to create a defaultdict for collecting counts of each hand that actually gets dealt.