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.