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.
Table of Contents
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