Combining C and Python#
The standard Python interpretter is written in C, and it’s possible to write Python modules in C or C++ to improve performance. This not strictly within the scope of the course, but if you’re interested, you can find more information in the Python documentation.
A concrete example#
As a (relatively) simple example, let’s look at a C++ function which takes a list of numbers and returns the sum of the squares of the numbers. We’ll then write a wrapper to call this function from Python.
The C++ code file#
The core C++ function#
Here’s a version of simple a C++ function to calculate the sum of the squares of a bunch of numbers.
//
// This is written in C style passing in an array and its length.
int sum_of_squares(int* numbers, int length){
int sum = 0;
for(int i=0; i<length; i++){
sum += numbers[i]*numbers[i];
}
return sum;
}
The Python wrapper#
The Python wrapper is a little more complicated, but not much. We need to include the Python header file, and then define a function which takes a Python object as an argument, and returns a Python object back to the interpretter. The cored function itself is very simple, it just converts the Python object into a C array, calls the C function, and then converts the result back into a Python object.
// These two lines are required to use the Python/C API
#define PY_SSIZE_T_CLEAN
#include <Python.h>
// A Python wrapper for the sum_of_squares function
static PyObject *
pysum_of_squares(PyObject *self, PyObject *args)
{
PyObject *py_list;
int length
int sum;
// Parse the Python input arguments (assumed to be a list of integers)
if (!PyArg_ParseTuple(args, "O", &py_list))
return NULL;
// Get the length of the list and assign memory for the C array.
length = PyList_Size(py_list);
int *input = new int[length];
// Convert the Python list to a C array
for(int i=0; i<length; i++){
input[i] = PyLong_AsLong(PyList_GetItem(py_list, i));
}
// Call the C function and get the result
sum = sum_of_squares(input, length);
// clean up the memory used by the C array
delete[] input;
return PyLong_FromLong(sum);
}
The method table#
The next step is to define a method table which tells Python how to call the function. This is a little more complicated than the previous examples, but not much. We need to define a method table which tells Python the name of the function, the C function to call, the type of the arguments, and a docstring to display when the user asks for help.
static PyMethodDef SquareSumMethods[] = {
{"sum_of_squares", // Python name
pysum_of_squares , // C function
METH_VARARGS, // Argument type (in Python.h)
"Sum the squares of the values in an iterable."}, // This is the docstring
{NULL, NULL, 0, NULL} // Sentinel
};
The module definition#
Finally, we need to define the module itself. This is mostly boilerplate, but we need to tell Python the name of the module, the docstring, and the method table we just defined.
To do this, we define a struct
with the required information, and then pass it to the PyModule_Create
function returned from a PyMODINIT_FUNC
function. These are both defined inside the Python header file.
static struct PyModuleDef square_sum_module = {
PyModuleDef_HEAD_INIT, // struct base from Python.h
"square_sum", // module name
"A module that sums the squares of the values in an iterable.", // docstring
-1, // size of per-interpreter state of the module, or -1 if the module keeps state in global variables.
SquareSumMethods // the array of methods created above
};
PyMODINIT_FUNC PyInit_square_sum(void) {
Py_Initialize();
return PyModule_Create(&square_sum_module);
}
The Python Setup File#
The most convenient way to build the module is to use a Python setup file. This is a Python script which uses the setuptools
module to build the module. The setup file for this example is shown below.
from setuptools import Extension, setup
setup(
ext_modules=[
Extension(
name="square_sum", # as it would be imported
# may include packages/namespaces separated by `.`
# don't need to use the same name as the module
sources=["sum_of_squares.cpp"], # all sources are compiled into a single binary file
),
]
)
Building the module#
To build the module, we need to run the setup.py
script. This can be done using the command
python setup.py build_ext --inplace
This will create a file called something likesquare_sum.so
,or square_sum.pyd
on Windows, which can be directly imported into Python (note that most versions actually include more details on the version of Python the import will work with) . The --inplace
option tells Python to put the file in the current directory, rather than in a subdirectory.
For this to work, you must have the Python development headers installed. On Ubuntu/WSL, this can be done using the command
sudo apt install python3-dev
Using the module#
Once the module is built, it can be imported into Python in the usual way a regular module file would. For example, if the module is called square_sum
, then we can use it as follows from a Python script/interpreter window.
>>> import square_sum
>>> square_sum.sum_of_squares([1,2,3,4,5])
55