Comparison in python , a tutorial

posted on by wael

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 method 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 .

comparison in python  : eq , ne , gt , lt , ge , le

The comparison methods

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 .

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

			
class A_Type_That_Does_Not_Implement_Equality_Operator:
    pass


>>> instanceOne = A_Type_That_Does_Not_Implement_Equality_Operator()                  
>>> instanceTwo = instanceOne                                     
>>> instanceThree = A_Type_That_Does_Not_Implement_Equality_Operator()  

              
>>> id(instanceOne)                                            
4417302600
>>> id(instanceTwo)                                            
4417302600
>>> id(instanceThree)                                          
4417739352


>>> instanceOne == instanceTwo                                    
True
>>> instanceOne == instanceThree                                  
False
>>> instanceOne != instanceThree                                  
True

# ### a type that does not implement __eq__ method ###
					

When we use enlighterobjectOne == ObjectTwo , if objectOne and objectTwo are of different types , and if objectTwo class is a subclass of the objectOne class , objectTwo __eq__ method is called with objectOne as an argument , otherwise objectOne __eq__ method is called with objectTwo as an argument.

			
class A_Class_That_Implements_Equality_Operator : 
    def __eq__(self , other):
        return True
        # returns True for all ==
>>> instanceOne = A_Class_That_Implements_Equality_Operator()
>>> objectOne = object()
# create an instance of the object class
>>> objectOne == instanceOne
# objectOne is an instance of the object class.
# instanceOne is an instance of a class that 
# is a subclass of the object class.
# objectOne and instanceOne are of different types.
# instanceOne __eq__ method is called.
# it returns true for all ==
# output 
True

>>> id(objectOne)
4488940256
>>> id(instanceOne)
4465172688


class A_Type_That_Does_Not_Implement_Equality_Operator:
    pass
>>> instance_not_implement_equal = A_Type_That_Does_Not_Implement_Equality_Operator()
>>> instance_implement_equal = A_Class_That_Implements_Equal()
>>> instance_not_implement_equal == instance_implement_equal
# instance_not_implement_equal  , has the object type __eq__ method
# instance_implement_equal is an instance of a class that is a subclass
# of the object class .
# instance_not_implement_equal and instance_implement_equal are of different 
# types.
# As such instance_implement_equal __eq__ method is called . 
# instance_implement_equal returns true for everything.
# output 
True 


class Another_Class_That_Implements_Equality_Operator : 
    def __eq__(self , other):
        return False
        # returns False for all ==
>>> another_instance_implement_equal = Another_Class_That_Implements_Equality_Operator()
>>> instance_implement_equal = A_Class_That_Implements_Equal()
>>> another_instance_implement_equal == instance_implement_equal
# another_instance_implement_equal is an instance of Another_Class_That_Implements_Equality_Operator
# instance_implement_equal is an instance of A_Class_That_Implements_Equal
# another_instance_implement_equal and instance_implement_equal are of different types.
# A_Class_That_Implements_Equal is not a subclass of Another_Class_That_Implements_Equality_Operator .
# As such another_instance_implement_equal __eq__ method is called . 
# another_instance_implement_equal returns False for everything
# output 
False 


# ### left object , right object , == method ### 
					

The object type __neq__ method compare two objects for inequality by calling the __eq__ method and inverting the result .

			
class A_Class_That_Implements_Equality_Operator : 
    def __eq__(self , other):
        return True
        # returns True for all ==
>>> instanceOne = A_Class_That_Implements_Equality_Operator()
>>> objectOne = object()
# create an instance of the object class .
>>> objectOne != instanceOne
# objectOne is an instance of the object class.
# instanceOne is an instance of a class that is a 
# subclass of the object class .
# objectOne and instanceOne are not of the same type .
# As such instanceOne __ne__ method is called . 
# instanceOne does not implement the __ne__ method ,
# as such  python will call the __ne__ method of the class
# that it extends . This is the object class. 
# The object class __ne__ methods calls the __eq__ method , and 
# invert the result. 
# So instanceOne __eq__ method is called
# with objectOne as an argument, and the result is inverted .
# instanceOne __eq__ method always return true , so __ne__
# will always return false.
# output 
False
# ### != , __ne__ ###
					

When we use enlighterobjectOne < ObjectTwo or enlighterobjectOne > objectTwo or enlighterobjectOne <= ObjectTwo or enlighterobjectTwo >= objectOne , and if objectTwo and objectOne are of different types , and if objectTwo class is a subclass of the objectOne class , the objectTwo __gt__ , __lt__ , __ge__ , __le__ methods are called with objectOne as an argument , otherwise objectOne __lt__ , __gt__ , __le__ , __ge__ methods are called and they will receive objectTwo as an argument .

			
class A_class_That_Implements_Less_Than:
    def __lt__(self, other):
        return True
        # A_class_That_Implements_Less_Than() < A_class_That_Implements_Less_Than() 
        # is always True 

class A_subclass_That_Implements_Greater_Than(A_class_That_Implements_Less_Than):
    def __gt__(self, other):
        return False
        # A_subclass_That_Implements_Greater_Than() > A_subclass_That_Implements_Greater_Than()
        # is always False

>>> instance_class_implements_lt = A_class_That_Implements_Less_Than()
>>> another_instance_class_implements_lt = A_class_That_Implements_Less_Than()
>>> instance_subclass_implements_gt = A_subclass_That_Implements_Greater_Than()
>>> instance_class_implements_lt < instance_subclass_implements_gt
False
# instance_class_implements_lt and instance_subclass_implements_gt are of different types
# instance_subclass_implements_gt is a subclass of instance_class_implements_lt
# instance_subclass_implements_gt __ge__ method is called .
# This method always return False
# output 
# False

>>> instance_class_implements_lt < another_instance_class_implements_lt
# instance_class_implements_lt and another_instance_class_implements_lt 
# are of the the same type 
# instance_class_implements_lt __le__ method is called 
# this method always return True
# output 
True
					

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 –address — of an object . When we implement the __eq__ method for a type, python will set its __hash__ method to None , and we must implement our own hash method .

			
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 e.g [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 e.g [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 the __eq__  method, an example

We can override the __eq__ , to provide our own comparison algorithm . In this example we want two words to be equal if and only they have the same length , and if they have the same letters , or if their letters differ by one. For example , we want ‘a’ and ‘a’ and ’12’ and ’21’ and ‘123’ and ‘214’ to be equal.

			
class Word:
    ''' The Type Word represents words in the english language .
        We define our own comparison algorithm .
        We consider two words to be equal iff :
            -   They have the same length , and they
                    differ by one letter , or are 
                    equals
    '''
    def __init__(self, letters):
        ''' @param letters : a string representing a word;
                             e.g = 'abc'
        '''
        self.letters = letters
    def __eq__(self, other):
        ''' The __eq__ method '''
        if not isinstance(other, Word):
            # if other is not an instance of words
            # return false
            return False
        if len(self.letters) != len(other.letters):
            # if self.letters and other.letters , do
            # not have the same length
            # return false
            return False
        word_one_letters_lowered = self.letters.lower()
        word_two_letters_lowered = other.letters.lower()
        # lower the letters of the two words
        count_of_difference_of_each_letter = {}
        for (index, selfLetter) in enumerate(word_one_letters_lowered):
            count_of_difference_of_each_letter[
                selfLetter] = count_of_difference_of_each_letter.setdefault(
                    selfLetter, 0) + 1
            # if selfLetter doesn't exist in the dictionary  initialize
            # it to zero.
            # Add one to its count
            otherLetter = word_two_letters_lowered[index]
            count_of_difference_of_each_letter[
                otherLetter] = count_of_difference_of_each_letter.setdefault(
                    otherLetter, 0) - 1
            # if otherLetter doesn't exist in the dictionary
            # initialize it to zero
            # Subtract one from the count
        sum_count_of_letters_difference_first_word = 0
        sum_count_of_letters_difference_second_word = 0
        for letter_difference_count in count_of_difference_of_each_letter.values():
            if letter_difference_count < -1 or letter_difference_count > 1:
                return False
            if letter_difference_count == 1:
                sum_count_of_letters_difference_first_word += 1
            if letter_difference_count == -1:
                sum_count_of_letters_difference_second_word += 1
            if sum_count_of_letters_difference_first_word > 1 or sum_count_of_letters_difference_second_word > 1:
                return False
        return True
        # for each letter , in both words letters  we calculate the count of
        # difference.
        # If the count of difference of each letter is 0 , then these two words
        # are equal .
        # Else , this means that we have letters which  have a count of difference larger
        # or less than 0 .
        # The two words are equal , if the  sum of (the count # of difference for each letter)
        # of the letters in each word is 0 or 1 , else they are not equal .

>>> empty_word = Word('')
>>> empty_word == ''
# empty_word and '' are of different types
# empty_word __eq__ method is called.
# It receives as an argument the right object .
# The Word Type __eq__ method  returns
# false , when the other object is not
# an instance of a Word
# output 
False


>>> first_word = Word('123')
>>> second_word = Word('1234')
>>> first_word == second_word
# Both of objects are instance of Word.
# first_word __eq__ method is called .
# Both of objects are instance of Word.
# They have different length , as such
# the __eq__ method returns False
# output 
False


>>> first_word = Word('123')
>>> second_word = Word('123')
>>> second_word == first_word 
# Both of objects are instance of Word.
# second_word __eq__ method is called and 
# is passed the first_word .
# Both objects are instance of Word.
# They have the same letters length.
# Their letters are lowercased .
# for each letter , in both words letters
# we calculate the count of difference.
# If the count of difference of each letter is 0 , 
# then these two words ar equal . 
# Else , this means  that we have letters which 
# have a count of difference larger or less than 0 .  
# The two words are equal , if the sum of (the  count 
# of difference for each letter) of the letters in each word 
# is 0 or 1 ,  else they are not equal . 
# output 
True
 

>>> first_word = Word('abc')
>>> second_word = Word('abe')
>>> first_word == second_word 
# Both of objects are instance of Word.
# first_word __eq__ method is called
# and is passed the second_word as 
# argument . Both words differ by one
# letter , as such they are equal .
True


>>> first_word = Word('121')
>>> second_word = Word('213')
>>> first_word != second_word
# Both of objects are instance of Word.
# first_word __ne__ method is called .
# first_word does not implement  __ne__
# method , as such it inherits the 
# object class __ne__ method .  
# object class __ne__ method  will call the
# Word __eq__ method  and apply the not
# operator on the result.
# Word('121') == Word('213')  is True as such
# Word('121') != Word('213') returns False
# Output
False

>>> first_word == second_word
# Word('121') == Word('213')
True
					

Implementing all the comparison methods , an example

In this example we will implement all the special comparison methods , they are :

  • __eq__ : ==
  • __ne__ : !=
  • __gt__ : >
  • __lt__ : <
  • __ge__ : >=
  • __le__ : <=

A SNumber is an integral number which is equal to another integral number if and only if the sums of the digits of each are equal . A SNumber is less than another integral number if and only if its sum of digits is less then the sum of digits of the other integral number.

			
class SNumber:
    ''' A SNumber is an integral number.
        It is equal to another integral number iff :
            - their sum of digits are equal .
        if is less than another integral number iff :
            - its sum of digits is less then the sum of digits
                of the other  integral number.
    '''
    def __init__(self, object):
        self.integral_number = int(str(object))
        # convert the object to string
        # convert the string to an integral number
        # using int
        # int('12') = 12
    def __str__(self):
        ''' _str__ method'''
        return str(self.integral_number)
    def sum_Of_Digits(self):
        ''' calculate the sum of digits of an SNumber'''
        return sum(int(digit) for digit in str(abs(self.integral_number)))
        # 1 - get the absolute value of self.integral_number
        # 2 - convert it to a string
        # 3 - create a generator object
        # 4 - convert each digit to int
        # 5 - sum the digits of self.integral_number
    def __eq__(self, other):
        '''__eq__ method'''
        if id(self) == id(other):
            # if the two object have the same id
            # they are equal
            return True
        other = SNumber(other)
        # convert other to an SNumber
        return self.sum_Of_Digits() == other.sum_Of_Digits()
        # Two SNumber are equal , if they have the same
        # sum of digits
    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 = SNumber(other)
        # convert other to an SNumber
        return self.sum_Of_Digits() > other.sum_Of_Digits()
        # greater than will return True , if the sum
        # of digits of this SNumber is larger than the
        # sum of digits of the other SNumber
    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)

>>> SNumber(0) <= 0
# 0 <= 0
True

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

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

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

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

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