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

mohamad wael
8 min readSep 1, 2021

--

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

Originally published at https://twiserandom.com on September 1, 2021.

--

--

No responses yet