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:
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.
It enters the try_rich_compare
function, there it tries to compare them again, first normal then reversed.
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.
(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).