Equality in PythonThe Equality operator (==) is a comparison operator in Python that compare values of both the operands and checks for value equality. Whereas the 'is' operator is the membership operator that checks whether both the operands refer to the same object or not (present in the same memory location).
Summary. Implement the Python __eq__ method to define the equality logic for comparing two objects using the equal operator ( == )
Two objects having equal values are not necessarily identical. Put simply: == determines if the values of two objects are equal, while is determines if they are the exact same object.
Consider this simple problem:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
So, Python by default uses the object identifiers for comparison operations:
id(n1) # 140400634555856
id(n2) # 140400634555920
Overriding the __eq__
function seems to solve the problem:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return False
n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3
In Python 2, always remember to override the __ne__
function as well, as the documentation states:
There are no implied relationships among the comparison operators. The truth of
x==y
does not imply thatx!=y
is false. Accordingly, when defining__eq__()
, one should also define__ne__()
so that the operators will behave as expected.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
In Python 3, this is no longer necessary, as the documentation states:
By default,
__ne__()
delegates to__eq__()
and inverts the result unless it isNotImplemented
. There are no other implied relationships among the comparison operators, for example, the truth of(x<y or x==y)
does not implyx<=y
.
But that does not solve all our problems. Let’s add a subclass:
class SubNumber(Number):
pass
n3 = SubNumber(1)
n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False
Note: Python 2 has two kinds of classes:
classic-style (or old-style) classes, that do not inherit from object
and that are declared as class A:
, class A():
or class A(B):
where B
is a classic-style class;
new-style classes, that do inherit from object
and that are declared as class A(object)
or class A(B):
where B
is a new-style class. Python 3 has only new-style classes that are declared as class A:
, class A(object):
or class A(B):
.
For classic-style classes, a comparison operation always calls the method of the first operand, while for new-style classes, it always calls the method of the subclass operand, regardless of the order of the operands.
So here, if Number
is a classic-style class:
n1 == n3
calls n1.__eq__
;n3 == n1
calls n3.__eq__
;n1 != n3
calls n1.__ne__
;n3 != n1
calls n3.__ne__
.And if Number
is a new-style class:
n1 == n3
and n3 == n1
call n3.__eq__
;n1 != n3
and n3 != n1
call n3.__ne__
.To fix the non-commutativity issue of the ==
and !=
operators for Python 2 classic-style classes, the __eq__
and __ne__
methods should return the NotImplemented
value when an operand type is not supported. The documentation defines the NotImplemented
value as:
Numeric methods and rich comparison methods may return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true.
In this case the operator delegates the comparison operation to the reflected method of the other operand. The documentation defines reflected methods as:
There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather,
__lt__()
and__gt__()
are each other’s reflection,__le__()
and__ge__()
are each other’s reflection, and__eq__()
and__ne__()
are their own reflection.
The result looks like this:
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is NotImplemented:
return NotImplemented
return not x
Returning the NotImplemented
value instead of False
is the right thing to do even for new-style classes if commutativity of the ==
and !=
operators is desired when the operands are of unrelated types (no inheritance).
Are we there yet? Not quite. How many unique numbers do we have?
len(set([n1, n2, n3])) # 3 -- oops
Sets use the hashes of objects, and by default Python returns the hash of the identifier of the object. Let’s try to override it:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
The end result looks like this (I added some assertions at the end for validation):
class Number:
def __init__(self, number):
self.number = number
def __eq__(self, other):
"""Overrides the default implementation"""
if isinstance(other, Number):
return self.number == other.number
return NotImplemented
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
x = self.__eq__(other)
if x is not NotImplemented:
return not x
return NotImplemented
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
class SubNumber(Number):
pass
n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)
assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1
assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1
assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1
assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
You need to be careful with inheritance:
>>> class Foo:
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
else:
return False
>>> class Bar(Foo):pass
>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False
Check types more strictly, like this:
def __eq__(self, other):
if type(other) is type(self):
return self.__dict__ == other.__dict__
return False
Besides that, your approach will work fine, that's what special methods are there for.
The way you describe is the way I've always done it. Since it's totally generic, you can always break that functionality out into a mixin class and inherit it in classes where you want that functionality.
class CommonEqualityMixin(object):
def __eq__(self, other):
return (isinstance(other, self.__class__)
and self.__dict__ == other.__dict__)
def __ne__(self, other):
return not self.__eq__(other)
class Foo(CommonEqualityMixin):
def __init__(self, item):
self.item = item
Not a direct answer but seemed relevant enough to be tacked on as it saves a bit of verbose tedium on occasion. Cut straight from the docs...
functools.total_ordering(cls)
Given a class defining one or more rich comparison ordering methods, this class decorator supplies the rest. This simplifies the effort involved in specifying all of the possible rich comparison operations:
The class must define one of __lt__()
, __le__()
, __gt__()
, or __ge__()
. In addition, the class should supply an __eq__()
method.
New in version 2.7
@total_ordering
class Student:
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With