Abstract Object API

So far we have talked about how to coerce Python objects to and from unsigned long objects; however, one of the benefits of Python is that it is a dynamic language. Being dynamic means we can program to an object’s interface and write code that accepts any object that matches that interface. This pattern is often called “duck typing”.

The C API also allows us to write code that is generic in this way by using the Abstract Object API. The Abstract Object API is broken up into a couple parts based on common Python protocols, for example, the mapping protocol or the iterator protocol. Some functions can act on any type, for example: PyObject_Repr()

There are performance trade offs to consider when using the Abstract Object API. The Abstract Object API requires a lot of runtime dispatching and object allocations so it will often be slower than the equivalent code using a more specific object’s API; but, it allows us to write functions which behave more like normal Python functions.

Generic Object Functions

Most of the builtin Python functions have an Abstract Object API equivalent. Generally the name is the same except it is PyObject_ClassCase instead of alllowercase.

Comparison

A weird quirk of the CPython API is that all of the comparison operators (>, >=, ==, etc...) are grouped into a single function called PyObject_RichCompare() instead of different functions like in Python. This is probably done to save space in the PyTypeObject struct.

This function accepts the two operands followed by a macro which specifies the operator to use. For example, to call a < b in the CPython API you would write:

PyObject* a;  /* ... */
PyObject* b;  /* ... */

PyObject* result = PyObject_RichCompare(a, b, Py_LE);
if (!result) {
    /* error handling */
}
/* do stuff with ``result`` */
Py_DECREF(result);

PyObject_RichCompare() can invoke arbitrary Python code so we need to check for errors. This will also raise an error if a and b cannot be compared because they are incompatible types.

Because it is common to want the result of a comparison as a boolean value, there is a helper function called PyObject_RichCompareBool() which returns a C int instead of a PyObject*. This saves us the hassle of worrying about cleaning up the reference to result. We could rewrite the above example with PyObject_RichCompareBool() as:

PyObject* a;  /* ... */
PyObject* b;  /* ... */

int result = PyObject_RichCompareBool(a, b, Py_LE);
if (result < 0) {
    /* error handling */
}

The function can still raise an exception, so we need to check for values less than 0 which signals that an error occurred.

Number Protocol

Unlike comparisons, there are different functions for all of the numeric operators. These are mostly named PyNumber_{Operator}, for example:

and so on.

Like the rest of the Abstract Object API, these functions are generic on their input and return new PyObject*s.

For a more complete list of Number API functions, see Number API.

Using the Number API in fib

One nice thing about Python int objects is that they can hold arbitrarily large integers. This is not true for unsigned long values which can store at most 2 ** 64 - 1. The Fibonacci sequence grows quickly and we will run out of room to store the results if we represent it as an unsigned long.

Open up fib.c and change it so that we store a and b in PyObject* values. Use the Number API functions to do arithmetic. We can continue to hold n in an unsigned long because that is a reasonable limit for the number of Fibonacci numbers we can compute.

Note

Remember to check for errors!

Extra Work

If you complete the exercise early, try to optimize fib by only using the boxed integer values when we know fib(n) will overflow.

This technique of having a typed fast path for common inputs with a generic fallback path dramatically speed up extensions.