19

You have a Python class which needs an equals test. Python should use duck-typing but is it (better/more accurate) to include or exclude an isinstance test in the eq function? For example:

class Trout(object):
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return isinstance(other, Trout) and self.value == other.value
amcnabb
  • 2,161
  • 1
  • 16
  • 24
Noel Evans
  • 8,113
  • 8
  • 48
  • 58

3 Answers3

16

Using isinstance in __eq__ methods is pretty common. The reason for this is that if the __eq__ method fails, it can fallback on an __eq__ method from another object. Most normal methods are called explicitly, but __eq__ is called implicitly, so it requires look-before-you-leap more frequently.

EDIT (thanks for the reminder, Sven Marnach):

To make it fallback, you can return the NotImplemented singleton, as in this example:

class Trout(object):
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if isinstance(other, Trout):
            return self.value == other.value
        else:
            return NotImplemented

Suppose a RainbowTrout knows how to compare itself to a Trout or to another RainbowTrout, but a Trout only knows how to compare itself to a Trout. In this example, if you test mytrout == myrainbowtrout, Python will first call mytrout.__eq__(myrainbowtrout), notice that it fails, and then call myrainbowtrout.__eq__(mytrout), which succeeds.

amcnabb
  • 2,161
  • 1
  • 16
  • 24
  • While this answer is true, it doesn't explain how to implement `__eq__()` in a way that actually facilitates a fallback to the other object's `__eq__()`. – Sven Marnach Mar 23 '12 at 17:59
9

Using isintsance() is usually fine in __eq__() methods. You shouldn't return False immediately if the isinstance() check fails, though -- it is better to return NotImplemented to give other.__eq__() a chance of being executed:

def __eq__(self, other):
    if isinstance(other, Trout):
        return self.x == other.x
    return NotImplemented

This will become particularly important in class hierarchies where more than one class defines __eq__():

class A(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        if isinstance(other, A):
            return self.x == other.x
        return NotImplemented
class B(A):
    def __init__(self, x, y):
        A.__init__(self, x)
        self.y = y
    def __eq__(self, other):
        if isinstance(other, B):
            return self.x, self.y == other.x, other.y
        return NotImplemented

If you would return False immediately, as you did in your original code, you would lose symmetry between A(3) == B(3, 4) and B(3, 4) == A(3).

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
5

The "duck-typing" principle is that you don't care what other is, as long as it has a value attribute. So unless your attributes share names with conflicting semantics, I'd suggest doing it like this:

def __eq__(self, other):
    try:
        return self.value == other.value
    except AttributeError:
        return False # or whatever

(Alternately you could test whether other has a value attribute, but "it's easier to ask forgiveness than to get permission")

alexis
  • 48,685
  • 16
  • 101
  • 161
  • 3
    This might result in something very silly, like `Trout("rainbow") == Lorikeet("rainbow")`, even though hardly anyone would consider fish equal to birds. – SingleNegationElimination Mar 23 '12 at 17:38
  • 1
    @Token, in that case duck typing is not right for you. These would only come out equal if they both store "rainbow" in the attribute "value", by the way. – alexis Mar 23 '12 at 22:01
  • ... and you define their `__eq__` method as the OP wants, to only look at the `value` field. But yes, that's the duck typing principle. Love it or leave it. – alexis Mar 23 '12 at 22:19
  • 1
    A rather significant drawback of this approach though is that the try/except is *much* slower (about 60% when I did a test). See https://gist.github.com/2950940 Arguably the try/except is more Pythonic, but I'd rather have fast non-pythonic code than slow pythonic code. :) – Adam Parkin Jun 18 '12 at 21:52
  • Python exceptions are designed to be very fast, but of course they won't be as fast as a simple type look-up. Unless you're using this for numeric processing, though, I'd be more worried about optimizing programmer productivity (read: extension-friendly design) than about shaving milliseconds. – alexis Jun 21 '12 at 19:03
  • Anyway the cost is incurred only when the `value` field is missing-- the rest of the time you should get a small speed-up from avoiding a test. – alexis Jun 21 '12 at 19:04