__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.
Trueclass 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
Trueclass A:
''' Class A defines the
__eq__ method. '''
def __eq__( self, other):
return Trueclass B( A):
''' class B extends the
class A, so it has
access to its
__eq__ method. '''
passclass 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:
Falseclass A:
''' class A defines the
__ne__ method'''
def __ne__( self, other):
return Trueclass B( A):
''' class B extends the
class A, as such it
has access to its
__ne__ method. '''
passclass 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 Falseclass 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 Trueclass Implements_Gt( Implements_Lt):
def __gt__( self, other):
return False
# always return falseclass 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)
282766077class 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
Originally published at https://twiserandom.com on September 1, 2021.