Implementation of a comparison of objects of the same class

We'll look at a simple same-class comparison by looking at a more complete BlackJackCard class:

class BlackJackCard:

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

def __lt__(self, other: Any) -> bool:
if not isinstance(other, BlackJackCard):
return NotImplemented
return self.rank < other.rank

def __le__(self, other: Any) -> bool:
try:
return self.rank <= cast(BlackJackCard, other).rank
except AttributeError:
return NotImplemented

def __gt__(self, other: Any) -> bool:
if not isinstance(other, BlackJackCard):
return NotImplemented
return self.rank > other.rank

def __ge__(self, other: Any) -> bool:
try:
return self.rank >= cast(BlackJackCard, other).rank
except AttributeError:
return NotImplemented

def __eq__(self, other: Any) -> bool:
if not isinstance(other, BlackJackCard):
return NotImplemented
return (self.rank == other.rank
and self.suit == other.suit)

def __ne__(self, other: Any) -> bool:
if not isinstance(other, BlackJackCard):
return NotImplemented
return (self.rank != other.rank
or self.suit != other.suit)

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

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

This example class defines all six comparison operators.

The various comparison methods use two kinds of type checking: class and protocol:

  • Class-based type checking uses isinstance() to check the class membership of the object. When the check fails, the method returns the special NotImplemented value; this allows the other operand to implement the comparison. The isinstance() check also informs mypy of a type constraint on the objects named in the expression.
  • Protocol-based type checking follows duck typing principles. If the object supports the proper protocol, it will have the necessary attributes. This is shown in the implementation of the __le__() and __ge__() methods. A try: block is used to wrap the attempt and provide a useful NotImplemented value if the protocol isn't available in the object. In this case, the cast() function is used to inform mypy that only objects with the expected class protocol will be used at runtime. 

There's a tiny conceptual advantage to checking for support for a given protocol instead of membership in a class: it avoids needlessly over-constraining operations. It's entirely possible that someone might want to invent a variation on a card that follows the protocol of BlackJackCard, but is not defined as a proper subclass of BlackjackCard. Using isinstance() checks might prevent an otherwise valid class from working correctly.

The protocol-focused try: block might allow a class that coincidentally happens to have a rank attribute to work. The risk of this situation turning into a difficult-to-solve problem is nil, as the class would likely fail everywhere else it was used in this application. Also, who compares an instance of Card with a class from a financial modeling application that happens to have a rank-ordering attribute?

In future examples, we'll focus on protocol-based comparison using a try: block. This tends to offer more flexibility. In cases where flexibility is not desired, the isinstance() check can be used.

In our examples, the comparison uses cast(BlackJackCard, other) to insist to mypy that the other variable  conforms to the BlackjackCard protocol. In many cases, a complex class may have a number of protocols defined by various kinds of mixins, and a cast() function will focus on the essential mixin, not the overall class.

Comparison methods explicitly return NotImplemented to inform Python that this operator isn't implemented for this type of data. Python will try reversing the argument order to see whether the other operand provides an implementation. If no valid operator can be found, then a TypeError exception will be raised.

We omitted the three subclass definitions and the factory function, card21(). They're left as an exercise.

We also omitted intraclass comparisons; we'll save that for the next section. With this class, we can compare cards successfully. The following is an example where we create and compare three cards:

>>> two = card21(2, "♠")
>>> three = card21(3, "♠")
>>> two_c = card21(2, "♣")

Given those three BlackJackCard instances, we can perform a number of comparisons, as shown in the following code snippet:

>>> f"{two} == {three} is {two == three}"
2♠ == 3♠ is False
>>> two.rank == two_c.rank True >>> f"{two} < {three} is {two < three}"
2♠ < 3♠ is True

The definitions seem to have worked as expected.

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

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