2

I tried the following experiment:

>>> class A(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return self.name
...     def __eq__(self, other):
...         print('{}.__eq__({})'.format(self, other))
...         return NotImplemented
... 
>>> a1 = A('a1')
>>> a2 = A('a2')
>>> a1 == a2
a1.__eq__(a2)
a2.__eq__(a1)
a1.__eq__(a2)
a2.__eq__(a1)
a2.__eq__(a1)
a1.__eq__(a2)
False

What the heck is going on here?

More generally, is there official documentation of the exact flow that occurs when evaluating an operator? The data model documentation alludes to some kind of fallback behavior but does not precisely describe what it is. There are several possibilities that complicate the situation:

  • a and b may be of the same or different types
  • a and b may or may not not define an __eq__ method
  • a.__eq__(b) or b.__eq__(a) may return NotImplemented, or another value.

Some kind of flow diagram would be helpful.


Edit: Before marking this question as a duplicate, please make sure the supposed duplicate answers the following:

  • Why does __eq__ get called 6 times in the given pattern?
  • Where is the behavior fully documented?
  • Can I get a flowchart?
augurar
  • 12,081
  • 6
  • 50
  • 65

2 Answers2

2

The behaviour is python-2.x only and it's part of how rich comparisons work internally (at least CPython) but only if both are new-style classes and both arguments have the same type!

The source C-code reads (I highlighted the parts where the comparisons are done and/or skipped):

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyObject *res;

    assert(Py_LT <= op && op <= Py_GE);
    if (Py_EnterRecursiveCall(" in cmp"))
        return NULL;

    /* If the types are equal, and not old-style instances, try to
       get out cheap (don't bother with coercions etc.). */
    if (v->ob_type == w->ob_type && !PyInstance_Check(v)) {
        cmpfunc fcmp;
        richcmpfunc frich = RICHCOMPARE(v->ob_type);
        /* If the type has richcmp, try it first.  try_rich_compare
           tries it two-sided, which is not needed since we've a
           single type only. */
        if (frich != NULL) {
            /****************************************************/
            /* 1. This first tries v.__eq__(w) then w.__eq__(v) */
            /****************************************************/
            res = (*frich)(v, w, op);
            if (res != Py_NotImplemented)
                goto Done;
            Py_DECREF(res);
        }
        /* No richcmp, or this particular richmp not implemented.
           Try 3-way cmp. */
        fcmp = v->ob_type->tp_compare;
        if (fcmp != NULL) 
            /***********************************************/
            /* Skipped because you don't implement __cmp__ */
            /***********************************************/
            int c = (*fcmp)(v, w);
            c = adjust_tp_compare(c);
            if (c == -2) {
                res = NULL;
                goto Done;
            }
            res = convert_3way_to_object(op, c);
            goto Done;
        }
    }

    /* Fast path not taken, or couldn't deliver a useful result. */
    res = do_richcmp(v, w, op);
Done:
    Py_LeaveRecursiveCall();
    return res;
}

/* Try a genuine rich comparison, returning an object.  Return:
   NULL for exception;
   NotImplemented if this particular rich comparison is not implemented or
     undefined;
   some object not equal to NotImplemented if it is implemented
     (this latter object may not be a Boolean).
*/
static PyObject *
try_rich_compare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;

    if (v->ob_type != w->ob_type &&
        PyType_IsSubtype(w->ob_type, v->ob_type) &&
        (f = RICHCOMPARE(w->ob_type)) != NULL) {
            /*******************************************************************************/
            /* Skipped because you don't compare unequal classes where w is a subtype of v */
            /*******************************************************************************/
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(v->ob_type)) != NULL) {
            /*****************************************************************/
            /** 2. This again tries to evaluate v.__eq__(w) then w.__eq__(v) */
            /*****************************************************************/
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(w->ob_type)) != NULL) {
            /***********************************************************************/
            /* 3. This tries the reversed comparison: w.__eq__(v) then v.__eq__(w) */
            /***********************************************************************/
        return (*f)(w, v, _Py_SwappedOp[op]);
    }
    res = Py_NotImplemented;
    Py_INCREF(res);
    return res;
}

The interesting parts are the comments - which answers your question:

  1. If both are the same type and new-style classes it assumes it can do a shortcut: it makes one attempt to rich compare them. The normal and reversed return NotImplemented and it proceeds.

  2. It enters the try_rich_compare function, there it tries to compare them again, first normal then reversed.

  3. A final attempt is made by testing the reversed operation: Now it compares it reversed and then attempts the normal (reversed of the reversed operation) one again.

  4. (not shown) In the end all 3 possibilities failed then a last test is done if the objects are identical a1 is a2 which returns the observed False.

The presence of the last test can be observed if you test a1 == a1:

>>> a1 == a1
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
True

I don't know if that behaviour is fully documented, at least there are some hints in the documentation of __eq__

A rich comparison method may return the singleton NotImplemented if it does not implement the operation for a given pair of arguments.

and __cmp__:

Called by comparison operations if rich comparison (see above) is not defined.


Some more observations:

Note that if you define __cmp__ it doesn't respect return NotImplemented like __eq__ does (because it enters the previously skipped branch in PyObject_RichCompare):

class A(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name
    def __eq__(self, other):
        print('{}.__eq__({})'.format(self, other))
        return NotImplemented
    def __cmp__(self, other):
        print('{}.__cmp__({})'.format(self, other))
        return NotImplemented


>>> a1, a2 = A('a1'), A('a2')
>>> a1 == a2
a1.__eq__(a2)
a2.__eq__(a1)
a1.__cmp__(a2)
a2.__cmp__(a1)
False

The behaviour with the subclasses or identical classes can be seen easily seen if you explicitly compare with superclass and inherited class:

>>> class A(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return self.name
...     def __eq__(self, other):
...         print('{}.__eq__({}) from A'.format(self, other))
...         return NotImplemented
...
>>>
>>> class B(A):
...     def __eq__(self, other):
...         print('{}.__eq__({}) from B'.format(self, other))
...         return NotImplemented
...
>>>
>>> a1, a2 = A('a1'), B('a2')
>>> a1 == a2
a2.__eq__(a1) from B
a1.__eq__(a2) from A
a1.__eq__(a2) from A
a2.__eq__(a1) from B
a2.__eq__(a1) from B
a1.__eq__(a2) from A
False
>>> a2 == a1
a2.__eq__(a1) from B
a1.__eq__(a2) from A
a1.__eq__(a2) from A
a2.__eq__(a1) from B
False

A final comment:

I added the code I used to "print" where it does the comparisons in a gist. If you know how to create python-c-extensions you can compile and run the code yourself (the myrichcmp function needs to be called with the two arguments to compare for equality).

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 2
    Great explanation. As you note, the last four comparisons are as defined by the docs: first try reversed comparison starting on the right (which may lead to trying from the left if the RHS returns NotImplemented) and then try ordinary comparison starting from the left (which may lead to trying from the right if the left returns NotImplemented). The first two are an optimization. I think an important takeaway from this (although it's not explicitly stated in the docs) is that *the exact number of times the compare functions are called is an implementation detail and is not defined behavior*. – BrenBarn Jan 12 '17 at 02:54
  • @augurar I've included all relevant source code into the question and highlighted the relevant comparisons and their order. If I need to explain anything else, let me know :) (But you won't get the flow chart xD) – MSeifert Jan 12 '17 at 04:06
  • Also worth noting that if one object does not define any of the rich comparison methods, the other object's `__eq__` method will only be called once, not twice. – augurar Jan 12 '17 at 09:07
-3

Please read Python Manual about NotImplemented: "Numeric methods and rich comparison methods should return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true."

That is if you change return NotImplemented by return self.name==other.name, there will be a single call to eq

Andrew
  • 720
  • 1
  • 6
  • 9
  • This does not answer the question. My question is: why does observed behavior occur? – augurar Jan 11 '17 at 22:33
  • I already wrote: because you returned NotImplemented from your code so "The interpreter will then try the reflected operation, or some other fallback, depending on the operator." Why 6 times? Because you used Python 2.7. Python 3.5.2 and 3.6.0 try only 2 variants of comparison. Possibly it is a bug in Python 2.7. – Andrew Jan 12 '17 at 09:31