Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning NotImplemented from __eq__

What's the result of returning NotImplemented from __eq__ special method in python 3 (well 3.5 if it matters)?

The documentation isn't clear; the only relevant text I found only vaguely refers to "some other fallback":

When NotImplemented is returned, the interpreter will then try the reflected operation on the other type, or some other fallback, depending on the operator. If all attempted operations return NotImplemented, the interpreter will raise an appropriate exception. See Implementing the arithmetic operations for more details.

Unfortunately, the "more details" link doesn't mention __eq__ at all.

My reading of this excerpt suggests that the code below should raise an "appropriate exception", but it does not:

class A:
  def __eq__(self, other):
    return NotImplemented

class B:
  def __eq__(self, other):
    return NotImplemented

# docs seems to say these lines should raise "an appropriate exception"
# but no exception is raised
a = A()
b = B()
a == b # evaluates as unequal
a == a # evaluates as equal

From experimenting, I think that when NotImplemented is returned from __eq__, the interpreter behaves as if __eq__ wasn't defined in the first place (specifically, it first swaps the arguments, and if that doesn't resolve the issue, it compares using the default __eq__ that evaluates "equal" if the two objects have the same identity). If that's the case, where in the documentation can I find the confirmation of this behavior?

Edit: see Python issue 28785

like image 828
max Avatar asked Nov 24 '16 07:11

max


2 Answers

Actually the == and != check work identical to the ordering comparison operators (< and similar) except that they don't raise the appropriate exception but fall-back to identity comparison. That's the only difference.

This can be easily seen in the CPython source code (version 3.5.10). I will include a Python version of that source code (at least as far as it's possible):

_mirrored_op = {'__eq__': '__eq__',  # a == b => b == a
                '__ne__': '__ne__',  # a != b => b != a
                '__lt__': '__gt__',  # a < b  => b > a
                '__le__': '__ge__',  # a <= b => b >= a
                '__ge__': '__le__',  # a >= b => b <= a
                '__gt__': '__lt__'   # a > b  => b < a
               }

def richcmp(v, w, op):
    checked_reverse = 0
    # If the second operand is a true subclass of the first one start with
    # a reversed operation.
    if type(v) != type(w) and issubclass(type(w), type(v)) and hasattr(w, op):
        checked_reverse = 1
        res = getattr(w, _mirrored_op[op])(v)     # reversed
        if res is not NotImplemented:
            return res
    # Always try the not-reversed operation
    if hasattr(v, op):
        res = getattr(v, op)(w)      # normal
        if res is not NotImplemented:
            return res
    # If we haven't already tried the reversed operation try it now!
    if not checked_reverse and hasattr(w, op):
        res = getattr(w, _mirrored_op[op])(v)      # reversed
        if res is not NotImplemented:
            return res
    # Raise exception for ordering comparisons but use object identity in 
    # case we compare for equality or inequality
    if op == '__eq__':
        res = v is w
    elif op == '__ne__':
        res = v is not w
    else:
        raise TypeError('some error message')

    return res

and calling a == b then evaluates as richcmp(a, b, '__eq__'). The if op == '__eq__' is the special case that makes your a == b return False (because they aren't identical objects) and your a == a return True (because they are).

However the behavior in Python 2.x was completely different. You could have up to 4 (or even 6, I don't remember exactly) comparisons before falling back to identity comparison!

like image 126
MSeifert Avatar answered Nov 02 '22 16:11

MSeifert


Not sure where (or if) it is in the docs, but the basic behavior is:

  • try the operation: __eq__(lhs, rhs)
  • if result is not NotImplemented return it
  • else try the reflected operation: __eq__(rhs, lhs)
  • if result is not NotImplemented return it
  • otherwise use appropriate fall back:

eq -> same objects? -> True, else False

ne -> different objects? -> True, else False

many others -> raise exception

The reason that eq and ne do not raise exceptions is:

  • they can always be determined (apple == orange? no)
like image 26
Ethan Furman Avatar answered Nov 02 '22 16:11

Ethan Furman