In Chapter 211 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 <=:
__eq__() for equality (==)
__ne__() for inequality (\=!)
__gt__() for greater than (>)
__ge__() for greater than or equal to (>=)
__lt__() for less than (<)
__le__() for less than or equal to (<=).
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
212,
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
73.
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.