How to Make Python Classes from Cpp Classes - danecross/PyNA62Analysis GitHub Wiki

If you want more general info on this topic, this is a condensed version of this.

Basics

CPython in runtime sees all Python Objects as variables of type PyObject*, which is our base type for all Python Objects. PyObject contains a counter and a pointer to the "type object," which points to the actual type definition.

If you want to define a new Extension type, you need to create a new type object.

Example: create a new type called "Custom"

#define PY_SSIZE_T_CLEAN
#include <Python.h>

Same imports as before.

typedef struct {
    PyObject_HEAD // 2
    /* Type-specific fields go here. */
} CustomObject;

Notes:

  1. In this case we are basically creating an empty object.
  2. This is a mandatory thing to add. It has to be there and it has to be the first thing. Note that there is no semicolon or comma.
static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)          // 1
    .tp_name = "custom.Custom",             // 2
    .tp_doc = "Custom objects",             
    .tp_basicsize = sizeof(CustomObject),   // 3
    .tp_itemsize = 0,                       // 4
    .tp_flags = Py_TPFLAGS_DEFAULT,         // 5
    .tp_new = PyType_GenericNew,            // 6
};

This is the definition of the type object. It basically makes something that we can register. Notes:

  1. This is a Mandatory line
  2. This is the name of the type as it can be imported into Python
  3. Tells compiler how much space to allocate for one object
  4. Compatibility information. Works for Python3
  5. This a default constructor for object creation. Since this is a basic class with no fancy attributes, PyType_GenericNew is ok. More on this later.

Next you define the Module and create the Init method. The module definition is the same as before.

The PyInit_custom() function has a few extra lines:

PyMODINIT_FUNC PyInit_custom(void){
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)                              // 1
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    PyModule_AddObject(m, "Custom", (PyObject *) &CustomType);     // 2
    return m;
}

Notes:

  1. This line initializes the "Custom" type
  2. This adds the type to the module Dictionary.

Adding Data and Methods

That previous example was not exciting. Let's discuss how to incorporate actual data and functions into our class.

Let our new typedef look like this:

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */ // 1
    PyObject *last;  /* last name */
    int number;
} CustomObject;

Notes:

  1. These are going to be Python Strings

There are two parts that we have to worry about now:

  1. The Data
  2. The Functions

Every new method that will be callable from Python should be registered in the CustomType list.

Dealing with Data

We need to be more careful about memory allocation. Namely, we should have:

  1. Destructor method
  2. Default Constructor
  3. Non-default Constructor
  4. Custom_Members list which exposes our instance variables as attributes callable in python

Destructor (Deallocation Method)

static void Custom_dealloc(CustomObject *self){
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

add to the .tp_dealloc attribute:

.tp_dealloc = (destructor) Custom_dealloc, // 1

Note:

  1. the (destructor) cast is to avoid a warning in the compilation

Default Constructor

static PyObject * Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds){
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

and add to the tp_new attribute:

.tp_new = Custom_new,

Note:

  1. A General note: we really only make a new() function when we want to specify PyObject types. In this case, we specify that self->first and self->last are Python Strings.

Non-default Constructor

static int Custom_init(CustomObject *self, PyObject *args, PyObject *kwds){
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

and add to the tp_init slot:

.tp_init = (initproc) Custom_init, 

Expose instance variables as attributes

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

and put these definitions into the tp_members slot:

.tp_members = Custom_members,

This is it for the data processing.

Dealing with functions

This is almost the same as just adding functions normally, but there are a few fancy hoops to jump through.

Let's define a function:

static PyObject * Custom_name(CustomObject *self){
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

Now all we have to do is:

  1. register the function
  2. put the registered functions list into the tp_methods slot.

Registering the Function

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

Put registered functions into the tp_methods slot

.tp_methods = Custom_methods,

Done! In total, the class looks like this:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    PyModule_AddObject(m, "Custom", (PyObject *) &CustomType);
    return m;
}
⚠️ **GitHub.com Fallback** ⚠️