Overriding definitions for immutable objects

The following is a simple class hierarchy that provides us with definitions of __hash__() and __eq__():

import sys
class
Card2:
insure = False

def __init__(self, rank: str, suit: "Suit", hard: int, soft: int) -> None:
self.rank = rank
self.suit = suit
self.hard = hard
self.soft = soft

def __repr__(self) -> str:
return f"{self.__class__.__name__}(suit={self.suit!r}, rank={self.rank!r})"

def __str__(self) -> str:
return f"{self.rank}{self.suit}"

def __eq__(self, other: Any) -> bool:
return (
self.suit == cast(Card2, other).suit
and self.rank == cast(Card2, other).rank
)

def __hash__(self) -> int:
return (hash(self.suit) + 4*hash(self.rank)) % sys.hash_info.modulus

class AceCard2(Card2):
insure = True

def __init__(self, rank: int, suit: "Suit") -> None:
super().__init__("A", suit, 1, 11)

This object is immutable in principle. There's no formal mechanism to the immutable instances. We'll look at how to prevent attribute value changes in Chapter 4, Attribute Access, Properties, and Descriptors.

Also, note that the preceding code omits two of the subclasses that didn't change significantly from the previous example, FaceCard and NumberCard.

The __eq__() method has a type hint, which suggests that it will compare an object of any class and return a bool result. The implementation uses a cast() function to provide a hint to mypy that the value of other will always be an instance of Card2 or a runtime type error can be raised. The cast() function is part of mypy's type hinting and has no runtime effect of any kind. The function compares two essential values: suit and rank. It doesn't need to compare the hard and soft values; they're derived from rank.

The rules for Blackjack make this definition a bit suspicious. Suit doesn't actually matter in Blackjack. Should we merely compare rank? Should we define an additional method that compares rank only? Or, should we rely on the application to compare rank properly? There's no best answer to these questions; these are potential design trade-offs.

The __hash__() function computes a unique value pattern from the two essential attributes. This computation is based on the hash values for the rank and the suit. The rank will occupy the most significant bits of the value, and suit will be the least significant bits. This tends to parallel the way that cards are ordered, with rank being more important than suit. The hash values must be computed using the sys.hash_info.modulus value as a modulus constraint.

Let's see how objects of these classes behave. We expect them to compare as equal and behave properly with sets and dictionaries. Here are two objects:

>>> c1 = AceCard2(1, '♣') 
>>> c2 = AceCard2(1, '♣') 

We defined two instances of what appear to be the same card. We can check the ID values to be sure that they're distinct objects:

>>> id(c1), id(c2)
(4302577040, 4302577296)
>>> c1 is c2
False 

These have different id() numbers. When we test with the is operator, we see that they're distinct objects. This fits our expectations so far.

Let's compare the hash values:

>>> hash(c1), hash(c2)
(1259258073890, 1259258073890)

The hash values are identical. This means that they could be equal.

The equality operator shows us that they properly compare as equal:

>>> c1 == c2
True 

Because the objects produce a hash value, we can put them into a set, as follows:

>>> set([c1, c2])
{AceCard2(suit=<Suit.Club: '♣'>, rank='A')}

Since the two objects create the same hash value and test as equal, they appear to be two references to the same object. Only one of them is kept in the set. This meets our expectations for complex immutable objects. We had to override both special methods to get consistent, meaningful results.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset