Python in Detail: Customizing Equality
Welcome to the Customizing Equality lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
In past lessons, we used
==to check equality and theisoperator to check identity. When we define a new class, equality checks with==behave exactly like identity checks withis. In other words, our objects are only equal to themselves.>
class Cat:def __init__(self, name):self.name = name- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
cat1 = Cat("Keanu")(cat1 == cat1, cat1 is cat1)Result:
(True, True)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
cat1 = Cat("Keanu")cat2 = Cat("Ms. Fluff")(cat1 == cat2, cat1 is cat2)Result:
(False, False)
This makes intuitive sense: every cat object
isitself, and is equal to itself. But this rule also means that no two cat objects are ever equal to each other, even if they have the same attributes.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
cat1 = Cat("Keanu")cat2 = Cat("Keanu")(cat1 is cat2, cat1 == cat2)Result:
(False, False)
Often, we want two different objects to be equal when their attributes are the same, like the two Keanus above. To achieve that, we need to override the default equality behavior.
When we check
point_a == point_b, Python actually callspoint_a.__eq__(point_b). We can define our own.__eq__to customize howPoints are compared for equality.The
.__eq__dunder method takes one argument: the object we're comparing to, often namedother. It returns a boolean, which determines the outcome of the comparison. In the next example, twoPoints are equal if theirxandycoordinates are equal.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __eq__(self, other):return self.x == other.x and self.y == other.y- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point_a = Point(5, 3)point_b = Point(5, 3)point_a == point_bResult:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point_a = Point(5, 3)point_b = Point(5, 3)point_a is point_bResult:
False
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point_a = Point(5, 3)point_c = Point(4, 4)point_a == point_cResult:
False
Note that comparisons generally shouldn't raise exceptions. For example, if we compare two built-in values that can't possibly be equal, we don't get a
TypeError. Instead, we getFalsebecause the values are different.>
"Hello" == 1Result:
False
Unfortunately, our current
Pointclass breaks this convention. The.__eq__method always tries to accessother.xandother.y, even ifotherdoesn't have those attributes. Accessing an attribute that doesn't exist raises anAttributeErrorexception.(You can type "error" when a code example will cause an error.)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point_a = Point(5, 3)point_a == "Hello"Result:
AttributeError: 'str' object has no attribute 'x'
The usual solution to this problem is to check the type of
otherusingisinstance. If it's not aPoint, we returnFalse.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __eq__(self, other):if not isinstance(other, Point):return Falsereturn self.x == other.x and self.y == other.y- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Point(5, 3) == Point(5, 3)Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Point(5, 3) == Point(1000, 2000)Result:
False
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Point(5, 3) == "Hello"Result:
False
This works, but sometimes we do want to compare different classes. For example, we might have a
Vectorclass withxComponentandyComponentattributes. APointand aVectorshould be equal ifpoint.x == vector.xComponentandpoint.y == vector.yComponent.Since these attributes have different names,
Vector.__eq__needs to checkother's type, handlingPoints andVectors separately.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __eq__(self, other):if not isinstance(other, Point):return Falsereturn self.x == other.x and self.y == other.yclass Vector:def __init__(self, x, y):self.xComponent = xself.yComponent = ydef __eq__(self, other):if isinstance(other, Point):return self.xComponent == other.x and self.yComponent == other.yelif isinstance(other, Vector):return (self.xComponent == other.xComponentand self.yComponent == other.yComponent)else:return False- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point = Point(5, 3)vector = Vector(5, 3)vector == pointResult:
True
That worked, but unfortunately there's a bug. If we compare in the other direction, we get the wrong answer.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point == vectorResult:
False
Equality should be symmetric: if
a == bis true thenb == ashould also be true. Unfortunately Python can't ensure symmetry for us; it's our job to ensure it.The problem is that we customized both classes'
.__eq__methods. When we comparevector == point, we're callingvector.__eq__(point), which works correctly. When we comparepoint == vector, we're callingpoint.__eq__(vector), butpoint.__eq__doesn't know about vectors.Fortunately, Python has an elegant solution to this problem:
NotImplemented. This is a special value that we return when.__eq__encounters a type that it doesn't support.First, here's the working code. Then we'll discuss why
NotImplementedsolves the problem.>
class Point:def __init__(self, x, y):self.x = xself.y = ydef __eq__(self, other):if not isinstance(other, Point):return NotImplementedreturn self.x == other.x and self.y == other.yclass Vector:def __init__(self, x, y):self.xComponent = xself.yComponent = ydef __eq__(self, other):if isinstance(other, Point):return self.xComponent == other.x and self.yComponent == other.yelif isinstance(other, Vector):return (self.xComponent == other.xComponentand self.yComponent == other.yComponent)else:return NotImplemented- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point = Point(5, 3)vector = Vector(5, 3)vector == pointResult:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
point == vectorResult:
True
When we compare
point == vector, Python callsPoint.__eq__(point, vector). That returnsNotImplementedbecauseotherisn't a point.NotImplementedtells Python that our dunder method isn't implemented for that type, and that Python should try the operation in a different way if possible. For the==operator, Python will try flipping the operands. Becausepoint == vectorisNotImplemented, Python triesvector == point. This callsVector.__eq__(vector, point), which returnsTrue.Note that this only happens if we return
NotImplemented. If we returnFalse, like we did in our original code, then we might get objects wherea == bis not the same asb == a.Here's a code problem:
We're working on a system for pet hotels where
OwnershaveCats. Owners have anemailand anid. Cats have anameand anid.Owners can change their email addresses, but their IDs are the ultimate marker of their identity. Two owner objects with the same ID should be equal even when their email addresses are different. Add an
.__eq__method to theOwnerclass that compares owners by id.Note that an
Ownerand aCatmight happen to have the same id. Make sure thatOwners can only be equal to otherOwners! (A hint: you can useisinstancefor this.)class Cat:def __init__(self, name, id):self.name = nameself.id = idclass Owner:def __init__(self, email, id):self.email = emailself.id = iddef __eq__(self, other):if not isinstance(other, Owner):return NotImplementedreturn self.id == other.id# Two Amirs with different email addresses, but they're the same user because# the IDs match.amir = Owner("amir@example.com", 1)amir2 = Owner("amir+nospam@example.com", 1)# Two people named Betty with the same email address, but they're different# users because the IDs are different.betty = Owner("betty@example.com", 2)betty2 = Owner("betty@example.com", 3)keanu = Cat("Keanu", 1)(amir == amir2, betty == betty2, amir == keanu)- Goal:
(True, False, False)
- Yours:
(True, False, False)
There are several other comparison methods in addition to
.__eq__. Each of these is customizable like.__eq__.__ne__is "not equal",!=.__lt__is "less than",<.__le__is "less than or equal to",<=.__gt__is "greater than",>.__ge__is "greater than or equal to",>=.
It's easy to look these up, so it's not important to memorize their names. Even if you don't memorize them, you may be able to guess them in the future by remembering that each comparison method's name contains exactly two letters.
Note that all of these dunder methods work with
NotImplemented. For example, ifa < breturnsNotImplemented, Python will tryb > a.Customizing
.__eq__is quite common in practice. Often, we want to compare our instances by equality, rather than using the simplistic identity comparison that we get by default. In many cases, equality is more subtle than "all of the attributes are the same." For example, in the code problem above, ourOwnerobjects only compared via IDs, ignoring email addresses.Which attributes we compare depends on the system, and how we think of the identity of individual objects. Unfortunately, that means that Python can never do it for us. But on the bright side,
.__eq__andNotImplementedare robust tools for implementing equality.