How to Build Comparable Classes in Python | by Lev Maximov | Apr, 2022

Fairly usually you’d need to make situations of a user-defined class comparable.

Suppose it’s good to take a look at that your operate returns the proper worth. If that worth occurs to be an object of some class, you’ll be able to run right into a shock:

however

When the == operator just isn’t overloaded, Python makes use of is operator as a fallback. It checks if two arguments are the identical object in reminiscence which is clearly the case within the first instance however not in the second.

To make the situations comparable ‘by worth’, not ‘by reference’, it’s good to overload the comparability operator.

After the equality operator is overloaded by defining the __eq__ method, the non-equality operator comes ‘freed from cost’ so the implementation of __ne__ methodology just isn’t required:

Now suppose it’s good to order a listing of objects (or discover a min or max of them):

One choice could be to make use of the ‘key’ argument:

It really works, however it’s laborious to inform if it really works appropriately except we overload the __repr__ methodology:

It is a viable choice, however in case the bottom line is at all times the identical a extra readable and fewer error-prone strategy could be to include the important thing into the category definition.

Now that the strict comparability operator is outlined

sorting will work ‘out of the field’:

Word that there’re extra operate calls right here so in case your __lt__ operate does one thing non-trivial, the ‘key’ strategy may work sooner.

In a fashion just like == and != operators, Python is wise sufficient to alleviate the burden of implementing each < and > for a category. In the event you ask for a ‘larger than’ comparability and have solely supplied the ‘lower than’ operator it should swap the arguments to get the outcome so implementing the __gt__ methodology just isn’t strictly vital:

It might appear logical at this level to imagine that Python is able to setting up the <= operator from the < and == we’ve supplied, however it isn’t:

On this case Python depends on ‘Specific is best than implicit’ rule.

One choice could be to outline the operation explicitly:

Identical to with the strict comparisons, Python guesses the which means of the so referred to as ‘mirrored’ operation:

Another choice is to make use of total_ordering decorator from the usual library:

One state of affairs the place counting on reflection can play a foul joke is inheritance.

As soon as once more, to make it work as anticipated, you both must implement all of the operators explicitly or use the total_ordering decorator:

It’s price noting although that because of the dynamic nature of Python the magic of each mirrored operations and total_ordering is occurring ‘within the runtime’ (=when the operator is known as, not when the category is outlined) so it comes at a value of a slight slow-down.

A sooner different is to make use of an already comparable class as a place to begin.

For instance, namedtuple implements all six operators for left-to-right comparability of its components:

If it’s good to additional prolong the performance of the category (add fields that needn’t participate compared or some additional strategies), you’ll be able to straight inherit from a listing

Or from a tuple (considerably extra contrived because it must be immutable):

The correct manner of implementing the comparability of the situations is to

  1. Overload all of the six operators.

    To be able to scale back the redundancy within the code, make it extra readable and fewer error-prone, it’s doable to:

  2. Implement solely a subset of the operations and let Python handle the remaining by utilizing the symmetries of the operators aka mirrored operations (e.g. a < b, thus b > a)
  3. Additional lower the variety of comparability operations and instruct Python to construct the rest via the functools.total_ordering decorator (e.g. a < b or a==b thus a <= b)
  4. Keep away from the specific implementation of the operators totally by counting on the code from the collections.namedtuple class (no customized fields or strategies).
  5. Reuse the operators from the listing or tuple implementation by inheriting from them (customized fields and strategies, however a bit extra code).

For fast experiments (2) and (3) are okay, however for time-critical functions (1), (4), or (5) are preferable.

P.S. The __cmp__ wealthy comparability methodology from Python 2.x (that returned a signed quantity and acted as a substitute for defining all 6 operations) impressed operator<=> in C++20, however didn’t discover its manner into Python3 with total_ordering being the official migration path.

More Posts