__eq__ , __ne__ , __lt__ , __gt__ , __ge__ , __le__ in python a tutorial

Everything in python is an object. An object has a type, which defines its attributes and methods. For example the str type, define the attributes and methods for a string object. An object has also an id, which is its memory address, and it has a value, for example 1 or a

To compare objects in python, we can implement python comparison methods __eq__ , __ne__ , __lt__ , __gt__ , __ge__ , __le__. In this article we will show how to implement these methods, and how they work.

The comparison methods

introduction

All types in python, extends the object type. As such when a type does not implement the __eq__, or __ne__ methods, the object type __eq__ and __ne__ methods are used.

The object type does not implement other comparison methods, such as less than __lt__, greater than __gt__, less or equal __le__, greater or equal __ge__, hence for an object to use these methods, its type must implement them.

__eq__

The object type __eq__ method, compares two objects for equality, by comparing their id, which is their memory address.

class Not_Implement_Eq:
    pass


>>> not_implement_eq = Not_Implement_Eq( )                  
>>> not_implement_eq_2 = not_implement_eq                                     
>>> not_implement_eq_3 = Not_Implement_Eq( )  

              
>>> id( not_implement_eq)                                            
4417302600
>>> id( not_implement_eq_2)                                            
4417302600
>>> id( not_implement_eq_3)                                          
4417739352


>>> not_implement_eq == not_implement_eq_2                                    
True
>>> not_implement_eq == not_implement_eq_3                                  
False
>>> not_implement_eq != not_implement_eq_3                                  
True

When we use objectOne == ObjectTwo, and if objectOne is an ancestor of ObjectTwo, then ObjectTwo __eq__ method is called, otherwise objectOne __eq__ method is called.

class Implements_Eq:
    def __eq__( self , other):
        return True
        # returns True for all ==

>>> implements_eq = Implements_Eq( )
# create an instance of the 
# Implements_Eq class
>>> an_object = object( )
# create an instance of the 
# object class

>>> id( an_object)
4488940256
>>> id( implements_eq)
4465172688

>>> an_object == implements_eq
# Implements_Eq class is a 
# descendant of the object class,
# hence implements_eq __eq__ method
# is called, it always return true.
True

>>> implements_eq == an_object
# The object class, is not a 
# descendant of the Implements_Eq
# class, hence implements_eq
# __eq__ method is called, it 
# always return true.
True



class Implements_Eq:
    def __eq__( self , other):
        return True
        # returns True for all ==

class Not_Implement_Eq:
    pass

>>> not_implements_eq = Not_Implement_Eq( )
# Create an instance of the
# Not_Implement_Eq class.
>>> implements_eq = Implements_Eq( )
# Create an instance of the
# Implements_Eq class.

>>> not_implements_eq == implements_eq
True



class A:
    ''' Class A defines the 
        __eq__ method. '''
    def __eq__( self, other):
        return True

class B( A):
    ''' class B extends the
        class A, so it has 
        access to its
        __eq__ method. '''
    pass

class C( A):
    ''' class C Extends the 
        class A, additionally
        it defines its own 
        version of the __eq__
        method.'''
    def __eq__( self, other):
        return False

>>> b = B( )
# Create an instance of B.
>>> c = C( )
# Create an instance of C.

>>> b == c
# b is an instance of
# B, c is an instance of
# C, C is not a descendant
# of B, as such b __eq__
# method is called, which
# is the A __eq__ method,
# and which always returns 
# true.
True

>> c == b
# b class is not a descendant
# of the c class, hence c
# __eq__ method is called, 
# this method always returns 
# false.
False

__ne__

x != y amounts to determining the class of x and y, and if x class is an ancestor of y class, then y __ne__ method is called, otherwise x __ne__ method is called.

The default object type __ne__ method, will call the __eq__ method, and negate its result, so the __ne__ operation, should be thought of as equivalent to !( x == y)

class Implements_Eq: 
    def __eq__( self, other):
        return True
        # returns True for all ==

>>> implements_eq = Implements_Eq( )
# create an instance of the
# Implements_Eq class. 
>>> an_object = object( )
# create an instance of the 
# object class.

>>> an_object != implements_eq
# Implements_Eq is a subclass
# of the object class, as such
# its __ne__ method is called.
# Its __ne__ method, is the one
# inherited from the object class,
# it first performs __eq__, 
# and later negate the result. 
# Implements_Eq __eq__ method
# always returns true, hence its
# negation is false, as such the
# result is:
False



class A:
    ''' class A defines the
        __ne__ method'''
    def __ne__( self, other):
        return True

class B( A):
    ''' class B extends the
        class A, as such it 
        has access to its
        __ne__ method. '''
    pass

class C( A):
    ''' class C extends A, and 
        additionally it defines
        its own version, of the
        __ne__ method.'''
    def __ne__( self, other):
        return False

>>> b = B( )
# Create an instance of B.
>>> c = C( )
# Create an instance of C.

>>> b != c
# C is not a descendant of
# B, hence b __ne__ method 
# is called. This is the 
# __ne__ method defined in
# the A class, and it always
# returns true.
True

>>> c != b
# b class is not a descendant
# of c class, hence c __ne__
# method is called, and it 
# always returns false.



class Implements_Ne:
    ''' A class which implements 
        the __ne__ method '''
    def __ne__( self, other):
        return False

class Not_Implements_Ne:
    ''' A class which does not
        implement the __ne__
        method '''
    pass

>>> implements_ne = Implements_Ne( )
>>> not_implements_ne = Not_Implements_Ne( )

>>> not_implements_ne != implements_ne
False

__lt__, __gt__, __le__, __ge__

When performing x < y, or x > y, or x <= y, or x >= y in python, this amounts to calling y __gt__, __lt__, __ge__, and __le__ methods, if y is a descendant of x, and y implements these methods, otherwise it amounts to calling x __lt__, __gt__, __le__, and __ge__ methods.

class Implements_Lt:
    def __lt__( self, other):
        return True
        # always return True 

class Implements_Gt( Implements_Lt):
    def __gt__( self, other):
        return False
        # always return false

class Not_Implements_Gt( Implements_Lt):
    pass

>>> implements_Lt = Implements_Lt( )
>>> implements_Gt = Implements_Gt( )
>>> not_implements_Gt = Not_Implements_Gt( )

>>> implements_Lt < implements_Gt
# Implements_Gt is a subclass of 
# Implements_Lt, as such Implements_Gt
# __gt__ method is called, it always
# returns false.
False

>>> implements_Lt < not_implements_Gt
# not_implements_Gt is a subclass 
# of Implements_Lt, but it does 
# not implement the __gt__ method, 
# as such the __lt__ method of 
# Implements_Lt is called, and it 
# always returns true.
True

The hash

If two objects are equal, their hash value must be equal. The hash of an object must not change for its lifetime, and it must be an int.

By default the object class, which every class in python extends, has its __hash__ method set to hash the id of an object. When the __eq__ method is implemented for a type, python will set this type __hash__ method, to None, and as such the hash method must be implemented.

class AClass:
    ''' By default Aclass extends the 
        object class.
        it has the object class: __eq__ 
        and __hash__ methods, since it 
        does not implements them.
    '''
    pass

>>> aClassIntance = AClass( )
>>> id( aClassIntance)
4524257232
>>> hash( aClassIntance)
282766077


class AClass:
    ''' By default, Aclass extends the 
        object class.
        We implemented the __eq__ 
        method.
        python sets Aclass __hash__ 
        method to None.
    '''
    def __eq__( self, other):
        return True
        # returns trues for all ==

>>> aClassIntance = AClass( )
>>> hash( aClassIntance)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: unhashable type: 'AClass'


class AbsoluteDistance:
    '''Calculate the absolute distance  
       between coordinates. 
       Coordinates is a list, as in
           [ 1, 2, 3, 4]
       Absolute distance of coordinates 
           is defined as: 
           coordinate      : [x, y, z ...]
           |coordinate|    : |x - y - z - ....|
    '''
    def __init__( self, coordinates):
        ''' Coordinates is a list for
            example: [ 1, 2, 3, 4].
            It must not be empty.
        '''
        if not isinstance( coordinates, list):
            raise TypeError( '[coordinates] must be a list')
            # raise a TypeError if coordinates 
            # is not a list
        if len( coordinates) == 0:
            raise ValueError( '[coordinates] must not be empty')
            # raise a ValueError if coordinates 
            # is empty
        self.__coordinates = coordinates.copy( )
        # create a copy of coordinates, and 
        # place it in the private attribute 
        # self.__coordinates
        # so that the value of the coordinates, 
        # and of the distance and of the hash 
        # does not change.
    def distance( self ):
        return abs( self.__coordinates[ 0] - sum( self.__coordinates[ 1:]))
        # |x - y - z - ....|  = 
        # |x - (y + z + ...)| = 
        # |x - sum( y + z ..)|
    def __eq__( self, other ):
        '''__eq__ method'''
        if id( self) == id( other):
            return True
            # If self and other are the 
            # same object, they are equal.
            # return true
        if not isinstance( other, AbsoluteDistance):
            return False 
            # if self and other are of 
            # different types return false.
            # They are not equal 
        return self.distance( ) == other.distance( )
        # AbsoluteDistance is equal when
        # self.distance( ) == other.distance( )
    def __hash__( self):
        '''hash method returns the distance'''
        return self.distance( )
        # Two AbsoluteDistance objects 
        # which are equal have the same
        # hash.
        # The hash does not change for 
        # the lifetime of the object.

>>> AbsoluteDistance([ 1, 2]) == AbsoluteDistance([ 1, 0])
True
>>> hash(AbsoluteDistance([ 1, 2])) == hash( AbsoluteDistance([ 1, 0]))
True

>>> AbsoluteDistance([ 1, 2]) == AbsoluteDistance([ -1, 0])
True
>>> hash( AbsoluteDistance([ 1, 2])) == hash( AbsoluteDistance([ -1, 0]))
True

>>> AbsoluteDistance([ 1, 2]) == AbsoluteDistance([ 4, 0])
False
>>> hash( AbsoluteDistance([ 1, 2])) == hash(AbsoluteDistance([ 4, 0]))
False

Implementing all the comparison methods, an example

class Anumber:
    def __init__( self, object):
        self.int_number = int( str( object))
        # convert the passed in
        # object, to string, after
        # that convert the string
        # to an integer number,
        # using int
        # int( '12') = 12
    def __str__( self):
        ''' _str__ method'''
        return str( self.int_number)
    def sum_Of_Digits( self):
        ''' calculate the sum of digits
            of an Anumber'''
        return sum( int( digit) for digit in str( abs( self.int_number)))
        # 1 - get the absolute value of
        #     self.int_number
        # 2 - convert it to a string
        # 3 - create a generator object
        # 4 - convert each digit to int
        # 5 - sum the digits of self.int_number
    def __eq__( self, other):
        '''__eq__ method'''
        if id( self) == id( other):
            # if the two objects have the
            # same id, they are equal
            return True
        other = Anumber( other)
        # convert other to an Anumber
        return self.sum_Of_Digits( ) == other.sum_Of_Digits( )
        # Two Anumbers are equal, if
        # their sum of digits are
        # equal.
    def __ne__( self, other):
        '''__ne__ method'''
        return not self.__eq__( other)
        # call __eq__  and inverse the
        # result
    def __gt__( self, other):
        if id( self) == id( other):
            # if they are the same
            # object, they are equal,
            # as such greater than
            # is false
            return False
        other = Anumber( other)
        # convert other to an
        # Anumber
        return self.sum_Of_Digits( ) > other.sum_Of_Digits( )
        # greater than will return
        # True, if the sum of digits
        # of this Anumber is larger
        # than the sum of digits of
        # the other Anumber
    def __ge__( self, other):
        '''__ge__ method
             greater or equal
        '''
        return self.__gt__( other) or self.__eq__( other)
    def __lt__( self, other):
        ''' __lt__
            not greater and not
            equals
        '''
        return not self.__ge__( other)
    def __le__( self, other):
        ''' __le__ method
            not greater
        '''
        return not self.__gt__( other)


>>> Anumber( 0) <= 0
# Anumber and 0, are of 
# different types, as such
# Anumber less or equal 
# method is called, 
# this amounts to
# 0 <= 0
True

>>> Anumber( 0) <= Anumber( '1')
# 0 <= 1
True

>>> Anumber( 12) >= '3'
# 3 >=  3
True

>>> Anumber( 12) >= Anumber( '33')
# 3 >= 6
False  

>>> Anumber( 11) == '2'
True

>>> Anumber( 11) != '2'
False