Equality for Python «
»


Code:
4 comments

A few days ago in #chipy, the chat room for the Chicago Python Users Group, we had a chat about how Python determines equality. It’s a pretty neat and extensible technique, so I’m going to walk through how I recently used it for playing cards.

Here’s the basic Card class. Note that I’m going to totally skip things like error-checking and documentation to keep the example obvious.

values = ('2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
suits = ('h', 'c', 'd', 's')
 
class Card:
    def __init__(self, value, suit):
        self.value, self.suit = value, suit
 
    def __repr__(self):
        return "<Card: %s%s>" % (self.value, self.card)

Man, does code get short when you don’t bother checking for errors. The usage is pretty clear, but there’s one odd issue:

>>> Card('3', 's')
<Card: 3s>
>>> Card('3', 's') == Card('3', 's')
False

Huh? That’s odd, an instance of Card doesn’t equal another Card just like itself? Well, let’s look at the Python docs. It talks a bit about comparing the builtin types (numbers, strings, lists…) and then says: “Most other types compare unequal unless they are the same object”.

Python does this by comparing the ids of the objects. You can call id() on your objects and see that even identically-constructed objects have different ids because they’re in different locations in memory. This is decent default because you wouldn’t want Python walking deeply through all of your objects, a potentially expensive operation. Python does do a little bit more for equality, as implied by that “most” in the documentation.

Python does one more thing for us. It looks for a function named __eq__ to call on the left-hand object and uses it to determine equality. So let’s add it to Card:

    def __eq__(self, card):
        return self.value == card.value and self.suit == card.suit

Easy enough. And usage:

>>> Card('3', 's') == Card('3', 's')
True
>>> Card('3', 's') == Card('K', 'h')
False
>>> Card('3', 's') != Card('3', 's')
True

Now that last one is a bit surprising. Back at the docs, we learn “There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false. Accordingly, when defining __eq__(), one should also define __ne__() so that the operators will behave as expected.” That’s:

    def __ne__(self, card):
        return not self.__eq__(card)

After that, the Cards equate properly and everything’s happy. One of the best things about Python is that it regularly gives you a sensible default and then lets you customize your code to work seamlessly with the language. This is what us Python fans mean when we go on and on about code being “Pythonic”.


Comments

  1. Alternately, in the constructor look up the card in a dictionary of already created cards (indexed by suit+rank). If it exists in the dictionary, return it. If it doesn’t, only then create it and add it to the dictionary. Now the same cards will compare equal since they really are ‘the same card’. Your solution is probably faster, mine more parsimonious with memory. If the constructor were expensive or called frequently the dictionary approach wins.

  2. Isn’t the sensible default for __ne__ to be implemented exactly as you have?

    .. def __ne__(self, rhs): return not self.__eq__(rhs)

    ??

    I realize there are cases where x == y does not imply x != y, but is not true that such situations represent the edge case and no the sensible default?

Leave a Reply

Your email address will not be published.