Not equal not not equal. By default.
Or: always implement __ne__ alongside __eq__
At our last Dojo session (Danilo has blogged about our Dojo, in Portuguese - I wonder when he'll switch to English?), the Kata being presented required knowing whether two instances of the same class were equal, based on some internal semantics. As in "object1 == object2". The coders went on, dutifully, and implemented the __eq__ method, which, in Python, is called whenever such a comparison is requested. Except that it's not enough. Or, perhaps more dangerously, it seems to be enough, but isn't.
You see, Python's operator overloading defines which methods are called when each specific operator is used. So, when you want to add two objects of a class you wrote, "a + b" will be converted into "a.__add__(b)", and all you have to do is define an __add__ method to your class that handles addition according to your class's semantics (there's a bit more to it, but that'll suffice for now).
Back to equality: using the "==" operator will result in a call to the "__eq__" method; if there is no such method in your class (nor was it inherited), the default is to compare the object's address in memory, or "id" (that is to say, equality comparison is true if, and only if, the objects are precisely the same). So far so good:
class Default:
pass
class Rich:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
The Default class just uses default comparison, and the Rich one checks for equality using a value passed to objects at the time of their creation:
>>> default_1 = Default()
>>> default_2 = Default()
>>> default_1 == default_2 # Different ids
False
>>> default_1 == default_1
True
>>> rich_1 = Rich(1)
>>> rich_2 = Rich(2)
>>> also_1 = Rich(1)
>>> rich_1 == rich_2 # Different values
False
>>> rich_1 == also_1 # Same values...
True
>>> rich_1 is also_1 # ... but different ids
False
Looks ok, and our Dojo coders got as far as that. Now we can tell if two objects are equal, but can we tell if they're different?
>>> rich_1 != rich_2
True
Fine. But what if they are supposed to be equal?
>>> rich_1 != also_1
True
That's the problem. When you define __eq__, you overload the "==" operator, but "!=" remains as the default, which is to check whether both objects are not located at the same address in memory! Objects rich_1 and also_1 have different addresses and thus compare as different when we look at them through !='s eyes, even though they're equal through =='s. So now we have objects that are equal and different at the same time!
>>> rich_1 == also_1, rich_1 != also_1
(True, True)
Fortunately, the solution is simple: we just have to define the __ne__ method to overload "!=". We don't even have to put the logic in there again, we can just make sure it's the opposite of what we're considering "being equal" to mean:
class Richer:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not (self == other)
Easy. Now the objects work as expected (at least as far as equality comparisons are concerned):
>>> richer_1 = Richer(1)
>>> alsoer_1 = Richer(1)
>>> richer_1 == alsoer_1
True
>>> richer_1 != alsoer_1
False
There.
Now, someone asked me why this behaviour (not __eq__) is not the default for __ne__.
I really have no idea. It seems to work even for the default case of __eq__ checking for the id. The Python documentation says: "There are no implied relationships among the comparison operators.
The truth of x==y does not imply that x!=y is false", but I can't see a practical reason for it since, for the vast majority of uses, x==y should imply that x!=y is false. I don't think "explicit is better than implicit" applies, since there is already an implicit assumption for !=, if you don't do anything explicit about that.
Anyone?