Python total_ordering definition class
Python Use total_ordering to define classes. The total_ordering
decorator is used to define operator classes that implement various comparison operations. It can be used with subclasses of numbers.Number
and semi-numeric classes.
The following uses playing cards as an example to illustrate the use of semi-numeric classes. Playing cards have points and suits, and the points play a crucial role in some gameplay. Similar to numbers, playing cards can be sorted and their points can be added together. However, multiplication between cards is meaningless, unlike numbers.
You can define a playing card class by inheriting from the NamedTuple
class, as follows:
from typing import NamedTuple
class Card1 (NamedTuple):
rank: int
suit: str
This definition looks good, but there’s a problem: all comparisons must include both the rank and suit, so when comparing the 2 of Spades and the 2 of Clubs, the following occurs:
>>> c2s = Card1(2, 'u2660')
>>> c2h = Card1(2, 'u2665')
>>> c2s
Card1(rank=2, suit='♣')
>>> c2h = Card1(2, 'u2665')
>>> c2h
Card1(rank=2, suit='♥')
>>> c2h == c2s
False
It’s easy to see that the default comparison method in this class isn’t suitable for many gameplay styles.
Most poker moves only require comparing points. A more practical definition is as follows:
from functools import total_ordering
from numbers import Number
from typing import NamedTuple
@total_ordering
class Card2(NamedTuple):
rank: int
suit: str
def __eq__(self, other: Any) -> bool:
if isinstance(other, Card2):
return self.rank == other.rank
elif isinstance(other, int):
return self.rank == other
return NotImplemented
def __lt__(self, other: Any) -> bool:
if isinstance(other, Card2):
return self.rank < other.rank
elif isinstance(other, int):
return self.rank < other
return NotImplemented
The Card2
class here inherits from the NamedTuple
class and uses the superclass’s __str__()
method to print instances as strings.
The class defines two comparison methods: one for equality and one for ordering. Other comparison methods, including __le__()
, __gt__()
, and __ge__()
, are implemented by @total_ordering
based on these two definitions. The inequality comparison method __ne__()
is generated by default based on __eq__()
and does not require any decorators.
The above methods implement two comparisons: between two Card2
objects, and between a Card2
object and an integer value. The type specifiers of the __eq__()
and __lt__()
parameters must be Any
to ensure compatibility with the parent class. Although writing it as Union[Card2, int]
is more precise, it will conflict with the parent class.
First, this class provides comparisons based solely on rank, as shown below:
>>> c2s = Card2(2, 'u2660')
>>> c2h = Card2(2, 'u2665')
>>> c2h == c2s
True
>>> c2h == 2
True
>>> 2 == c2h
True
This class can be used to compare playing cards based on rank. The decorator automatically generates various comparison operators, as shown below:
>>> c2s = Card2(2, 'u2660')
>>> c3h = Card2(3, 'u2665')
>>> c4c = Card2(4, 'u2663')
>>> c2s <= c3h < c4c
True
>>> c3h >= c3h
True
>>> c3h > c2s
True
>>> c4c != c2s
True
There’s no need to manually write comparison operators here; the decorator automatically generates them. However, the generated operators aren’t entirely ideal. In this example, problems arise when comparing integers and Card2
objects.
Due to limitations in the operator resolution mechanism, operations like c4c > 3
and 3 < c4c
will result in a TypeError
exception, which total_ordering
can’t correctly handle. While such cases are rare in real-world applications, when such comparisons are necessary, all comparison operators should be defined manually, rather than using the @total_ordering
decorator to automatically generate them.
Object-oriented programming isn’t the opposite of functional programming; in many cases, the two are complementary. Python’s ability to create immutable objects perfectly aligns with the requirements of functional programming. We can avoid creating objects with complex state while encapsulating related methods together. This is especially true when class attributes contain complex calculations. Encapsulating these calculations within the class definition makes the application logic easier to understand.