Search code examples
pythoncpython-c-api

How to define class methods in Python C API?


I was trying to create a Python class definition in C++ code and access it in Python. However, the function is called but the parameters are not received correctly. Please help me in doing this properly.

#include <iostream>
#include <Python.h>

using namespace std;

#include <Python.h>

static PyObject* MyClass__Init(PyObject *self, PyObject *args)
{
    cout << "MyClass__Init Called" << endl;
    Py_INCREF(Py_None);
    return Py_None;
};

static PyObject* MyModule__Start(PyObject *self, PyObject *args)
{
    const char* zBuff;

    if (PyArg_ParseTuple(args, "s", &zBuff))
        cout << "MyModule Start Called with parameter " << zBuff << endl;
    else
        cout << "MyModule Start ERROR" << endl;

    Py_INCREF(Py_None);
    return Py_None;
};

static PyObject* MyClass__Start(PyObject *self, PyObject *args)
{
    const char* zBuff;

    if (PyArg_ParseTuple(args, "s", &zBuff))
        cout << "MyClass Start Called with parameter" << zBuff << endl;
    else
        cout << "MyClass Start ERROR" << endl;

    Py_INCREF(Py_None);
    return Py_None;
};

static PyMethodDef pModuleMethods[] =
{
    {"Start", MyModule__Start, METH_VARARGS, ""},
    {NULL, NULL, 0, NULL}
};

static PyMethodDef pClassMethods[] = 
{
    {"__init__", MyClass__Init, METH_VARARGS, ""},
    {"Start", MyClass__Start, METH_VARARGS, ""},
    {NULL, NULL, 0, NULL}
};

void Start()
{
    Py_Initialize();

    /* create a new module and class */
    PyObject *pClassDic = PyDict_New();
    PyObject *pClassName = PyString_FromString("MyClass");
    PyObject *pClass = PyClass_New(NULL, pClassDic, pClassName);

    PyObject *pModule = Py_InitModule("MyModule", pModuleMethods);
    PyObject *pModuleDic = PyModule_GetDict(pModule);

    /* add methods to class */
    for (PyMethodDef* pDef = pClassMethods; pDef->ml_name != NULL; pDef++)
    {
        PyObject *pFunc = PyCFunction_New(pDef, NULL);
        PyObject *pMethod = PyMethod_New(pFunc, NULL, pClass);
        PyDict_SetItemString(pClassDic, pDef->ml_name, pMethod);
    }

    PyDict_SetItemString(pModuleDic, "MyClass", pClass);

    PyRun_SimpleString("import MyModule\n"
        "MyModule.Start('Hello Module')\n"
        "myObj = MyModule.MyClass()\n"
        "myObj.Start('Hello Class')\n");

    Py_Finalize();
};

int main()
{
    Start();
};

Output is,

MyModule Start Called with parameter Hello Module
MyClass__Init Called
MyClass Start ERROR

The Module function is called without any issue, but the class method is called without the proper input variable.


Solution

  • It appears that the parameter to self is always NULL and instead - for class methods - the reference to self is passed within the argument list. So, to parse the arguments of a class method you need to parse the reference to self, too.

    Since Python 2.6 you can provide a list of format specifiers to PyArg_ParseTuple. The number of format specifiers has to fit the number of arguments that are passed to the function (see https://docs.python.org/2/c-api/arg.html under (items) (tuple) [matching-items]).

    By modifying your MyClass__Start function to parse an additional parameter, you are able to parse and print both arguments to inspect them. For me, the following code

    static PyObject* MyClass__Start(PyObject *self, PyObject *args)
    {
        PyObject* argListSelf;
        const char* zBuff;
    
        if (PyArg_ParseTuple(args, "Os", &argListSelf, &zBuff)) {
            cout << "MyClass Start Called with parameters " <<  
            cout << PyString_AsString(PyObject_Str(argListSelf)) <<
            cout << " and " << zBuff << endl;
    
            cout << "self " << PyString_AsString(PyObject_Str(self)) << endl;
        }   
        else {
            if(PyErr_Occurred())
                PyErr_Print();
    
            cout << "MyClass Start ERROR" << endl;
        }   
    
        Py_INCREF(Py_None);
        return Py_None;
    };
    

    results in

    MyModule Start Called with parameter Hello Module
    MyClass__Init Called
    MyClass Start Called with parameters \
        0x602428<?.MyClass instance at 0x7f484a333200>0x602428 and HelloClass
    self <NULL>
    

    Note that I printed the pointer value of self, which is NULL. I also added

    if(PyErr_Occurred())
        PyErr_Print();
    

    which I would always add for debugging purposes.