[Python-ideas] A macro for easier rich comparisons

Petr Viktorin encukou at gmail.com
Wed Mar 18 11:53:52 CET 2015


Hello,
Rich comparison functions of many builtin types include a block of 
boilerplate code with a switch on the "op" argument, like this example 
from tupleobject:

     int cmp;
     PyObject *res;
     switch (op) {
     case Py_LT: cmp = vlen <  wlen; break;
     case Py_LE: cmp = vlen <= wlen; break;
     case Py_EQ: cmp = vlen == wlen; break;
     case Py_NE: cmp = vlen != wlen; break;
     case Py_GT: cmp = vlen >  wlen; break;
     case Py_GE: cmp = vlen >= wlen; break;
     default: return NULL; /* cannot happen */
     }
     if (cmp)
         res = Py_True;
     else
         res = Py_False;
     Py_INCREF(res);
     return res;

Each time it's implemented slightly differently: sometimes there are two 
values to compare, sometimes there's a cmp-style 0/1/-1 result to 
process; invalid "op" values are handled in various ways.

Such code is also required in third-party extensions.

I propose adding a public macro to ease this. My version takes two 
C-orderable values and the operation, similar to richcmpfunc:

#define Py_RICHCOMPARE(val1, val2, op) ( \
     ((op) == Py_EQ) ? PyBool_FromLong((val1) == (val2)) : \
     ((op) == Py_NE) ? PyBool_FromLong((val1) != (val2)) : \
     ((op) == Py_LT) ? PyBool_FromLong((val1) < (val2)) : \
     ((op) == Py_GT) ? PyBool_FromLong((val1) > (val2)) : \
     ((op) == Py_LE) ? PyBool_FromLong((val1) <= (val2)) : \
     ((op) == Py_GE) ? PyBool_FromLong((val1) >= (val2)) : \
     (Py_INCREF(Py_NotImplemented), Py_NotImplemented))

(As for the behavior for unknown op: for most cases the best thing to do 
is setting an error and returning NULL, but that doesn't fit into a 
macro. A surprising number of richcmpfunc's in CPython either return 
NULL without error (e.g. tupleobject), or fall through and return 
True/False arbitrarily (as in bytearrayobject). Datetime does an assert.
I think Py_NotImplemented is a good value to return)


I've made a patch with the macro and its use in CPython itself.
Read it as examples of usage: I'm not saying that all these modules 
should start using it immediately (or ever, e.g. for speed reasons), and 
if they do, many could use some refactoring in other code paths.
The patch is here: 
https://github.com/encukou/cpython/compare/master...richcmp


Why I'm interested in this: When porting old code to Python 3, rich 
comparisons a major cause of added boilerplate – especially when 
wrapping a C library that exposes either trivially comparable types or 
"cmp" functions. With this macro, a typical trivial rich comparison 
function reduces to:

     static PyObject* mytype_richcmp(PyObject *obj1, PyObject *obj2, int op)
     {
         if (mytype_Check(obj1) && mytype_Check(obj2)) {
             return PY_RICHCOMPARE(mytype_get_data(obj1), 
mytype_get_data(obj2), op);
         }
         Py_RETURN_NOTIMPLEMENTED;
     }

... or with `PY_RICHCOMPARE(mytype_cmp(obj1, obj2), 0, op)`.

(Note that even adding this to 3.5/3.6, would help in porting efforts: 
the macro itself is be easy to backport, but there are big benefits in 
having a standard name, known semantics, and official documentation.)


Is this a PEP-worthy idea?


More information about the Python-ideas mailing list