In Python, you can customize what happens when you ask objects whether they're equal to each other.
Here we have a Point
class:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
And we have two Point
objects (two instances of the Point
class):
>>> a = Point(1, 2)
>>> b = Point(1, 2)
If we ask these two objects whether they're equal, Python tells us False
, even though they have the same attributes:
>>> a == b
False
But if we ask one object whether it's equal to itself, Python tells us True
:
>>> a == a
True
The default implementation for equality in Python does the same thing as identity.
So we were really asking, are these two objects exactly the same object in memory:
>>> a is b
False
>>> a is a
True
==
operator work behind the scenes in Python?If we understand how the ==
operator works in Python, we can customize what happens when we ask two objects whether they're equal.
When ==
is used between two objects, Python calls the __eq__()
method on the first object, passing in the second object.
>>> a.__eq__(b)
NotImplemented
If it gets back True
or False
, it returns that value back to us.
But if it gets NotImplemented
, it then goes to the second object and asks whether it's equal to the first object:
>>> b.__eq__(a)
NotImplemented
If it gets True
or False
at this point, it returns that value back to us.
But if it gets NotImplemented
this time, it returns False
, because neither of these two objects know how to compare themselves to the other object.
Because a
doesn't know how to compare itself to b
and b
doesn't know how to compare itself to a
, these two values are not equal:
>>> a == b
False
__eq__
methodIf we wanted to implement equality on our Point
class (to customize what it means for Point
objects to be equal to other objects) we could make a __eq__
method in our Point
class:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
Our __eq__
method needs to accept self
and another object to compare to (we'll call it other
).
Our __eq__
method will make a tuple of our x
and y
attributes and compare it to the same tuple for the other
object:
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
If our object's x
and y
attributes are equal to the other
object's x
and y
attributes, then these two objects will be seen as equal.
So if we take two Point
objects (a
and b
) that have the same x
and y
values:
>>> a = Point(1, 2)
>>> b = Point(1, 2)
And we ask Python whether they're equal, we'll now get True
:
>>> a == b
True
But if we compare two Point
objects that don't have the same x
and y
values, we'll get False
:
>>> b = Point(1, 2)
>>> c = Point(1, 3)
>>> b == c
False
But we're not done yet!
Our __eq__
method is not very well-behaved right now.
If we pass in an object that isn't a Point
object, something weird happens.
For example if we passed in None
, we get an AttributeError
:
>>> a == None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/trey/point.py", line 10, in __eq__
return (self.x, self.y) == (other.x, other.y)
AttributeError: 'NoneType' object has no attribute 'x'
We're getting an AttributeError
because that other type of object doesn't have x
and y
attributes.
There's a number of ways to solve this problem, but the recommended way is to rely on type-checking.
If other
is an instance of our Point
class, we'll compare other
and self
as we did before.
But other
isn't a Point
, we'll return NotImplemented
:
def __eq__(self, other):
if isinstance(other, Point):
return (self.x, self.y) == (other.x, other.y)
return NotImplemented
This might seem a little bit weird because it's uncommon to see type-checking used in Python (we usually practice duck typing instead). But inside of dunder methods is one of the few places where type-checking is deemed acceptable.
Inside our __eq__
method, we really need to ask ourselves: do we know how to compare ourselves to that other
object?
If we don't, we should go delegate that other
object instead (and that's what NotImplemented
is for).
So now if we compare our Point
objects to None
, we get False
(as expected):
>>> a == None
False
If you'd like to customize what it means for instances of your class to be equal to some other object, you can define a __eq__
method.
But make sure that that your __eq__
method returns NotImplemented
when given an object that you don't know how to compare yourself to.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.