In Chapter 21 I discussed that objects can be aliases of each other, but that you can also make actual copies. What happens if you try to compare them?

class Point:
    def __init__( self, x=0.0, y=0.0 ):
        self.x = x
        self.y = y
    def __repr__( self ):
        return "({}, {})".format( self.x, self.y )

p1 = Point( 3, 4 )
p2 = Point( 3, 4 )
p3 = p1
print( p1 is p2 )
print( p1 is p3 )
print( p1 == p2 )
print( p1 == p3 )

The keyword is compares object identities. Since p3 is an alias for p1, p1 is p3 returns True, while p1 is p2 returns False. However, since p1 and p2 refer to the same point in 2D space, it would be nice if p1 == p2 would return True, i.e., that the == would do a value comparison (as you would expect). It does not. That is not surprising, as Python does not know how to compare the values of Points, and therefore the == does the only comparison that Python knows how to do, namely an identity comparison. However, you can instruct Python how to compare two points using the == operator, by defining an __eq__() method:

class Point:
    def __init__( self, x=0.0, y=0.0 ):
        self.x = x
        self.y = y
    def __repr__( self ):
        return "({}, {})".format( self.x, self.y )
    def __eq__( self, p ):
        return self.x == p.x and self.y == p.y

p1 = Point( 3, 4 )
p2 = Point( 3, 4 )
p3 = p1
print( p1 is p2 )
print( p1 is p3 )
print( p1 == p2 )
print( p1 == p3 )

The __eq__() method tells Python, in this case, what to do when two objects of the type Point are compared with ==. It returns True when their x and y coordinates are equal, False otherwise. In this example, the interpretation of the comparison operator == is “overloaded” by defining the __eq__() method.

You can also overload the other comparison operators \=!, >, >=, <, and <=:

If you specify the __eq__() method but not the __ne__() method, the __ne__() method will automatically return the opposite of what the __eq__() method returns. None of the other methods have such an automatic interpretation.

You are not limited to comparing only objects that are instances of the same class. For instance, when I define a class Quaternion that implements a quaternion, I might want to compare a quaternion with an integer or a float. That is possible:

class Quaternion:
    def __init__( self, a, b, c, d ):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
    def __repr__( self ):
        return "({},{}i,{}j,{}k)".format( self.a, self.b,
            self.c, self.d )
    def __eq__( self, n ):
        if isinstance( n, int ) or isinstance( n, float ):
            if self.a == n and self.b == 0 and \
                self.c == 0 and self.d == 0:
                return True
            else:
                return False
        elif isinstance( n, Quaternion ):
            if self.a == n.a and self.b == n.b and \
                self.c == n.c and self.d == n.d:
                return True
            else:
                return False
        return NotImplemented

c1 = Quaternion( 1, 2, 3, 4 )
c2 = Quaternion( 1, 2, 3, 4 )
c3 = Quaternion( 3, 0, 0, 0 )
if c1 == c2:
    print( c1, "==", c2 )
else:
    print( c1, "!=", c2 )
if c1 == c3:
    print( c1, "==", c3 )
else:
    print( c1, "!=", c3 )
if c3 == 1:
    print( c3, "==", 1 )
else:
    print( c3, "!=", 1 )
if c3 == 3:
    print( c3, "==", 3 )
else:
    print( c3, "!=", 3 )
if c3 == 3.0:
    print( c3, "==", 3.0 )
else:
    print( c3, "!=", 3.0 )
if c3 == "3":
    print( c3, "== \"3\"" )
else:
    print( c3, "!= \"3\"" )
if 3 == c3:
    print( 3, "==", c3 )
else:
    print( 3, "!=", c3 )

The implementation of the __eq__() method in the code above checks if the value the comparison is made with is a Quaternion, an integer, or a float. If so, it makes the comparison and returns True or False. If not, it returns NotImplemented. NotImplemented is a special value that indicates that the comparison has no sensible outcome. While the __ne__() method automatically inverts the result of the __eq__() method, it will not (and cannot) invert NotImplemented.

The last comparison in the code above is noteworthy. It executes comparison 3 == c3. Normally, when the comparison operator is defined, it will be executed for the left operand, i.e., the comparison operator of the integer 3 is executed, with c3 as argument. However, integers have not defined the __eq__() method for Quaternion, and thus this returns NotImplemented. If that happens, Python inverts the operands, so in this case executes the comparison c3 == 3. This comparison leads to a result as for Quaternion the comparison with an integer is defined. The same happens with the \=! operator. Something similar is done for the other comparison operators, but when the operands are inverted, < is swapped with >, and <= is swapped with >=, just as you would expect.

In Chapter 21, a Rectangle class was defined. Add to this class operators to test for equality of rectangles (two rectangles are equal if they have exactly the same shape), and greater/smaller operators (a rectangle is smaller than another rectangle if it has a smaller surface area). Test the new operators. Note: I am a bit on the fence on whether these are acceptable definitions for equality and the other comparisons, but for practice they are okay.

There is one special comparison I want to bring up, and that is testing whether an object is True or False. Many objects are considered to be False in particular circumstances; for instance, and empty list evaluates to False. This was briefly discussed in Chapter 7.

buffer = []
if buffer:
    print( buffer )
else:
    print( "buffer is empty" )

You can define your own evaluation of an object that is called when the object is used as condition. This is the __bool__() method.

__bool__() is called when an object is treated as a condition. It must return True or False. If __bool__() is not implemented, __len__() is called (see below), which will evaluate to False if __len__() returns zero. If neither __bool__() nor __len__() is implemented, the object is always True when used as condition.