Skip to main content

Modules/_operatormodule.c

Source:

cpython 3.14 @ ab2d84fe1023/Modules/_operatormodule.c

_operatormodule.c implements the operator standard library module entirely in C. Most of its surface area is a thin mapping from Python function calls to the CPython abstract object API (PyNumber_*, PyObject_*, PySequence_*). The non-trivial work lives in three callable object types: attrgetter, itemgetter, and methodcaller.

Map

SymbolKindLines (approx)Purpose
op_add_impl through op_xor_implfunctions60-350Arithmetic, bitwise, and sequence wrappers
operator_eq_impl through operator_le_implfunctions351-420Comparison wrappers via PyObject_RichCompareBool
attrgetter_objectstruct421-460Dotted attribute getter state
attrgetter_newfunction461-510Split dotted name on ., store parts tuple
attrgetter_callfunction511-560Chain PyObject_GetAttr over parts
itemgetter_objectstruct561-600Single-item or multi-item subscript getter
itemgetter_callfunction601-650Single vs tuple return variant
methodcaller_newfunction651-710Store name, args, kwargs
methodcaller_callfunction711-760Look up name, call with stored args
PyMemberDef tablesdata761-800Module method registration

Reading

eq/ne/lt and the PyObject_RichCompareBool mapping

Comparison functions in _operatormodule.c are the thinnest wrappers in the file. Each one calls PyObject_RichCompareBool with a fixed op constant and converts the C int result back to a Python bool.

// CPython: Modules/_operatormodule.c:356 operator_eq_impl
static PyObject *
operator_eq_impl(PyObject *module, PyObject *a, PyObject *b)
{
return PyBool_FromLong(PyObject_RichCompareBool(a, b, Py_EQ));
}

ne, lt, le, gt, and ge follow the same pattern, each passing the corresponding Py_NE, Py_LT, etc. constant. PyObject_RichCompareBool handles None comparisons and calls __eq__ (or the appropriate slot) on the left operand with a fallback to the right.

attrgetter_new: dotted path splitting

When the name string contains a ., attrgetter_new splits it and stores the parts as a Python tuple of interned strings. A plain name is stored as a single string object, not a tuple, so the common case remains allocation-free at call time.

// CPython: Modules/_operatormodule.c:468 attrgetter_new
static PyObject *
attrgetter_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
...
if (PyUnicode_Find(attr, dot, 0, PyUnicode_GET_LENGTH(attr), 1) < 0) {
/* No dot: store the name directly */
ag->attr = Py_NewRef(attr);
ag->nattrs = 1;
} else {
ag->attr = PyUnicode_Split(attr, dot, -1);
ag->nattrs = PyList_GET_SIZE(ag->attr);
}
...
}

attrgetter_call: chained PyObject_GetAttr

At call time the getter walks the stored parts tuple, feeding the output of one GetAttr as the input to the next. An error on any segment short-circuits the loop and propagates the AttributeError unchanged.

// CPython: Modules/_operatormodule.c:524 attrgetter_call
static PyObject *
attrgetter_call(attrgetterobject *ag, PyObject *args, PyObject *kw)
{
PyObject *obj = PyTuple_GET_ITEM(args, 0);
Py_INCREF(obj);
for (Py_ssize_t i = 0; i < ag->nattrs; i++) {
PyObject *attr = PyTuple_GET_ITEM(ag->attr, i);
PyObject *newobj = PyObject_GetAttr(obj, attr);
Py_DECREF(obj);
if (newobj == NULL) return NULL;
obj = newobj;
}
return obj;
}

itemgetter_call: single vs tuple variant

itemgetter dispatches on nitems at construction time to choose between two call paths. The single-item path calls PyObject_GetItem and returns the result directly. The multi-item path allocates a fresh tuple and fills it with one GetItem call per stored key.

// CPython: Modules/_operatormodule.c:614 itemgetter_call
static PyObject *
itemgetter_call(itemgetterobject *ig, PyObject *args, PyObject *kw)
{
PyObject *obj = PyTuple_GET_ITEM(args, 0);
if (ig->nitems == 1) {
return PyObject_GetItem(obj, ig->item);
}
PyObject *retval = PyTuple_New(ig->nitems);
if (retval == NULL) return NULL;
for (Py_ssize_t i = 0; i < ig->nitems; i++) {
PyObject *item = PyObject_GetItem(obj, PyTuple_GET_ITEM(ig->items, i));
if (item == NULL) { Py_DECREF(retval); return NULL; }
PyTuple_SET_ITEM(retval, i, item);
}
return retval;
}

methodcaller_new and methodcaller_call

methodcaller_new interns the method name string and stores the positional arguments as a tuple and keyword arguments as a dict. methodcaller_call resolves the attribute on the passed object and calls it with the stored arguments using PyObject_Call.

// CPython: Modules/_operatormodule.c:720 methodcaller_call
static PyObject *
methodcaller_call(methodcallerobject *mc, PyObject *args, PyObject *kw)
{
PyObject *obj = PyTuple_GET_ITEM(args, 0);
PyObject *method = PyObject_GetAttr(obj, mc->name);
if (method == NULL) return NULL;
PyObject *result = PyObject_Call(method, mc->args, mc->kwds);
Py_DECREF(method);
return result;
}

gopy notes

Status: not yet ported.

Planned package path: module/operator/. The arithmetic and comparison wrappers are mechanical and can be generated from the PyMemberDef table. attrgetter dotted-name splitting will use strings.SplitN. operator.contains calls PySequence_Contains, which returns -1 on error, 0 for absent, 1 for present; the Go port maps this to (bool, error). methodcaller.__reduce__ must be ported to pass pickle round-trip tests.