Overloading the six comparison operators works in a similar way to the overloading of arithmetic operators. The operators and their associated methods are:

Equals | x == y | x.__eq__(y) |

Not equals | x != y | x.__ne__(y) |

Less than | x < y | x.__lt__(y) |

Less than or equal to | x <= y | x.__le__(y) |

Greater than | x > y | x.__gt__(y) |

Greater than or equal to | x >= y | x.__ge__(y) |

## Default behaviour

Before we try overloading these operators, it’s worth having a look at their default behaviour. To illustrate, we define a class representing a rectangle in the file Rectangle.py:

class Rectangle: """Representation of a rectangle""" def __init__(self, length, width): self.length = length self.width = width self.area = length * width

We assume that the dimensions of the rectangle are both ints, so we don’t have to worry about round-off error, as we would with floats.

Now we test this class with the code in main.py:

from Rectangle import * rec1 = Rectangle(1, 12) rec2 = Rectangle(3, 4) rec3 = Rectangle(4, 5) print(rec1 == rec2) print(rec1 != rec2) print(rec1 < rec3)

We might think that all the print statements would raise errors, since we haven’t overloaded any of the operators to handle Rectangle objects. However, the output we get is:

False True Traceback (most recent call last): File "D:\Documents\Programming\programmingpages\Python\Rectangle comparison 01\Rectangle comparison 01\Rectangle_comparison_01.py", line 9, in <module> print(rec1 < rec3) TypeError: '<' not supported between instances of 'Rectangle' and 'Rectangle'

The == and != operators do *not* raise errors; they each give a valid boolean result. The < operator, however *does *raise an error, which is more in line with what we’d expect.

Python implements a default behaviour for both == and !=. Recall that everything in Python is an object. Each object is assigned its own unique ID number, which we can access by using the `id()`

method. Thus `id(rec1)`

returns the ID number assigned to the object `rec1`

. The default behaviour of == and != is to compare the IDs of their operands. In our code above, `rec1`

, `rec2`

and `rec3`

are all distinct objects so they have different IDs, thus == will always return False and != will always return True when comparing different objects, regardless of what data they may contain.

The other comparison operators, however, have no default behaviour, so attempting to use them in your own classes will always raise errors unless you overload the operators.

## Overloading == and !=

Suppose we wish to define comparison operators for the Rectangle class so that they compare the areas of the rectangles. Thus two Rectangle objects are ‘equal’ if their areas are equal, one Rectangle is ‘less than’ another if its area is smaller, and so on. To this end, we add an overload for ==, as shown:

class Rectangle: """Representation of a rectangle""" def __init__(self, length, width): self.length = length self.width = width self.area = length * width def __eq__(self, value): if isinstance(value, Rectangle): print('Calling == with Rectangle') return self.area == value.area

As usual, `self`

refers to the left operand and `value`

to the right operand. We check that value is a Rectangle, and then return a boolean value depending on the equality of their areas. I’ve inserted a `print()`

statement here which will be useful in a minute.

We now test this with the code:

from Rectangle import * rec1 = Rectangle(1, 12) rec2 = Rectangle(3, 4) rec3 = Rectangle(4, 5) print(rec1 == rec2) print(rec1 == rec3) print(rec1 != rec2) print(rec1 != rec3)

The output is

Calling == with Rectangle True Calling == with Rectangle False Calling == with Rectangle False Calling == with Rectangle True

This might be a bit of a surprise. As we’ve overloaded only the == operator, we might expect that the default behaviour (comparing IDs) would be used for the != operator, but from the output, we see that all four comparisons called the overloaded == operator. The two comparisons using == work correctly, as `rec1`

and `rec2`

have the same area so `rec1 == rec2`

should be True, while `rec1 == rec3`

should be False.

However, if we were comparing IDs rather than areas, `rec1 != rec2`

should be True, but the output shows it as False, which is correct if we’re comparing areas. In the case where an overloaded == operator is provided but an overloaded != operator is not, Python will check to see if an overload for != exists and, if not, it will call the overloaded == and then flip the result from True to False, and vice versa.

Oddly, if we provide an overloaded != operator but *not* an overloaded == operator, the == operator will then fall back to the default behaviour and compare IDs rather than areas. The moral of the story is that it’s always safest to provide your own overloads for *both* == and != just to be sure you’re getting what you want.

Here’s a version of Rectangle with full overloads for both == and !=, expanded to cover comparison with ints. We take a Rectangle to be equal to an int if its area has that value.

class Rectangle: """Representation of a rectangle""" def __init__(self, length, width): self.length = length self.width = width self.area = length * width def __eq__(self, value): if isinstance(value, Rectangle): print('Calling == with Rectangle') return self.area == value.area elif isinstance(value, int): print('Calling == with int') return self.area == value def __ne__(self, value): print('Calling !=') return not self == value

Since != should return the opposite of ==, we’ve just called `__eq__`

from `__ne__`

and applied the `not`

operator to the result. I’ve left in some `print()`

statements so we can trace the code; obviously you’d remove these when the code is used for real.

We can test this class with the code:

from Rectangle import * rec1 = Rectangle(1, 12) rec2 = Rectangle(3, 4) rec3 = Rectangle(4, 5) print(rec1 != rec2) print(rec1 != rec3) print(rec3 == 20) print(rec3 != 20) print(20 == rec3)

We get the output:

Calling != Calling == with Rectangle False Calling != Calling == with Rectangle True Calling == with int True Calling != Calling == with int False Calling == with int True

We see that we get the correct results.

The last statement `print(20 == rec3)`

shows that we can reverse the order of the arguments so that the calling object is on the right. For arithmetic operators, we needed a special overload to handle this case, but for comparison operators, Python automatically switches the operands to do the comparison. Thus `print(20 == rec3)`

is converted to `print(rec3 == 20)`

.

## Overloading <, <=, >, >=

The other four comparison operators can be overloaded in the same way. Here’s a complete Rectangle class with all 6 operators overloaded:

class Rectangle: """Representation of a rectangle""" def __init__(self, length, width): self.length = length self.width = width self.area = length * width def __eq__(self, value): if isinstance(value, Rectangle): print('Calling == with Rectangle') return self.area == value.area elif isinstance(value, int): print('Calling == with int') return self.area == value def __ne__(self, value): print('Calling !=') return not self == value def __lt__(self, value): if isinstance(value, Rectangle): print('Calling < with Rectangle') return self.area < value.area elif isinstance(value, int): print('Calling < with int') return self.area < value def __le__(self, value): print('Calling <=') return self < value or self == value def __gt__(self, value): print('Calling >') return not self <= value def __ge__(self, value): print('Calling >=') return not self < value

As many of the operators are logical combinations or opposites of other operators, we can condense the code a bit by delegating some comparisons to others. Here, we provide basic definitions for == and <, and then all other comparisons can be expressed in terms of these two.

Here are a few tests of this code:

from Rectangle import * rec1 = Rectangle(1, 12) rec2 = Rectangle(3, 4) rec3 = Rectangle(4, 5) print(rec1 < rec2) print(rec3 >= 12) print(12 < rec3)

This generates the output:

Calling < with Rectangle False Calling >= Calling < with int True Calling > Calling <= Calling < with int Calling == with int True

The statement `print(rec1 < rec2)`

calls the `__lt__`

and, since the areas of `rec1`

and `rec2`

are equal, returns False.

The statement `print(rec3 >= 12)`

calls `__ge__`

which in turn calls `__lt__`

with an int right operand, and returns True since the area of `rec3`

is 12.

The final statement `print(12 < rec3)`

illustrates how Python handles the calling object being the right operand. As with ==, the operands are swapped, but this time the < operator must be changed to >. This results in `__gt__`

being called with `self`

being `rec3`

and `value`

being 12. As the area of `rec3`

is 20, this returns True.

## Exercise

Write a Vector class with one data field storing a list of components for the vector and another data field for the magnitude. [You can extend the Vector class from the post on arithmetic operators if you like, although this exercise doesn’t use any of the methods from that class.] The magnitude of a vector is its length, calculated as

Overload all 6 comparison operators so that the comparison is done by comparing the magnitudes of the vectors. The magnitudes should be floats, so you will need to deal with round-off error when deciding whether two vectors have equal magnitudes. Review the `isclose()`

method from the post on floats to see how to do this. You should use a fairly large relative tolerance (say, or so) to allow vectors with slightly different magnitudes to be judged as ‘equal’.

To test the Vector class, write a program which generates a number of 3-dimensional Vectors (that is, with 3 components each) with each component being a random float between 0 and 1. The program should then count up the number of pairs of vectors that satisfy each of the 6 comparisons. The left operand in each comparison should precede the right operand in the list of Vectors. For example, if we had 4 Vectors in the order [W, X, Y, Z], compare W with each of X, Y and Z, then X with Y and Z, and finally Y with Z. When you’ve done all the comparisons, print out the totals for each of the 6 operators.

Note that for Vectors, you will need to do comparisons, so the number of comparisons increases proportionally to the *square* of the number of Vectors. Thus you’ll find that the program will probably run quite slowly if you input a large value of .

One possible definition for the Vector class is:

from math import * class Vector(object): """vector with magnitude comparisons""" relTol = 1e-3 def __init__(self, components): self.components = components self.mag = sqrt(sum(map(lambda a: a ** 2, components))) def __eq__(self, value): if isinstance(value, Vector): return isclose(self.mag, value.mag, rel_tol = Vector.relTol) def __ne__(self, value): if isinstance(value, Vector): return not self == value def __lt__(self, value): if isinstance(value, Vector): return not self == value and self.mag < value.mag def __gt__(self, value): if isinstance(value, Vector): return not self == value and self.mag > value.mag def __le__(self, value): if isinstance(value, Vector): return self == value or self.mag < value.mag def __ge__(self, value): if isinstance(value, Vector): return self == value or self.mag > value.mag

On line 9, we use the `map()`

function together with a lambda function (see this post) to produce a list of the squared components of the Vector. We then use Python’s built-in `sum()`

function to add up the squares, and then the `sqrt()`

function from the `math`

module to get the square root. You can do the same thing using loops if you like.

The overloaded == operator on line 11 returns True if the magnitudes of the two Vectors satisfy the `isclose()`

criterion with a relative tolerance of . You can adjust this value to see what effect it has on the results.

All the other overloaded operators can be expressed using the overloaded == together with the built-in operators for comparing floats. If you’re certain that comparisons will always be done between two Vectors (and not between a Vector and a float, say), you can omit all the if `isinstance(value, Vector):`

lines.

A suitable program to compare pairs from a list of Vectors is:

from Vector import * from random import * while True: numVec = input('How many vectors? ') if numVec == 'quit': break numVec = int(numVec) vecList = [] for i in range(numVec): vecList += [Vector([random(), random(), random()])] compareDict = {'eq':0, 'ne':0, 'lt':0, 'gt':0, 'le':0, 'ge':0} for i in range(numVec): for j in range(i + 1, numVec): if vecList[i] == vecList[j]: compareDict['eq'] += 1 else: compareDict['ne'] += 1 if vecList[i] <= vecList[j]: compareDict['le'] += 1 else: compareDict['gt'] += 1 if vecList[i] < vecList[j]: compareDict['lt'] += 1 else: compareDict['ge'] += 1 print(compareDict)

Lines 9 to 11 create the list of Vectors with random components. One way of storing the results is in a dictionary, so we define this on line 13. The comparisons are done with two nested `for`

loops. I’ve grouped the comparisons in pairs, so, for example, two vectors are either equal or not equal, or one vector is either greater than or equal to another, or it is less than, and so on.

The sum of the number of pairs in each complementary pair of operations should add up to the total number of comparisons.

A typical run of the program produces the output:

How many vectors? 1000 {'eq': 1036, 'ne': 498464, 'lt': 253446, 'gt': 245018, 'le': 254482, 'ge': 246054} How many vectors? quit

For randomly generated vectors, we’d expect the number of ‘lt’ pairs to be roughly equal to the number of ‘ge’ pairs, and the number of ‘le’ pairs to be about the same as the number of ‘gt’ pairs. Since it’s much more likely that two vectors will have different lengths, we’d expect the number for ‘eq’ to be significantly less than for ‘ne’. These results seem to verify these expectations.