1

I am attempting to call a function print_stuff which is a member of the class Spam from a user-defined Python module Spam in C++

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int main()
{
    Py_Initialize();

    //modify python module search path
    auto modulePath = /* hardcoded search path for module */
    PyObject* sysPath = PySys_GetObject("path");
    PyList_Append(sysPath, PyUnicode_FromString(modulePath));

    PyObject* pModule = PyImport_ImportModule("Spam");
    PyObject* pDict = PyModule_GetDict(pModule);
    PyObject* pFunc = PyObject_GetAttrString(pDict, "print_stuff");
    
    if (pFunc != NULL)
        PyObject_CallObject(pFunc, NULL);
    else
        PyErr_Print();
    
    return 1;
}

My Python module consists of 2 files; Spam.py and __init__.py both located in same directory /Spam which is a subfolder of the directory containing my VS solution and C++ code.

Spam.py

class Spam:
    def __init__(self):
        pass
    def print_stuff(self):
        print((1,2,3))

__init__.py

import Spam as Spam

When I run my C++ code I get the following error: AttributeError: 'dict' object has no attribute 'print_stuff'

I have tried getting the contents of the dictionary returned from PyModule_GetDict(pModule) as a string and confirmed that it doesn't mention the function.

I looked up the docs for PyModule_GetDict which says that it returns the same dictionary as you would get calling __dict__ on the module in Python. When I tried calling Spam.__dict__ inside of __init__.py I got the same result, my function was missing, but when I tried Spam.Spam.__dict__ I got a dictionary containing the function.

From this I decided to change the import statement to from Spam import Spam so that Spam.__dict__ would now include my function, which it did. However, nothing changes when I run my C++ code. I still get the same error from my dictionary and am still unable to call my function.

I did wonder if it was an issue of not having an instance of the class in the my C++ code, so I also tried removing the surrounding class and having Spam.py just contain the function definition and then importing that but, again, I got the same error.

I have a hunch this is some kind of namespace issue, otherwise I don't see how PyModule_GetDict and __dict__ could return different dictionaries for the same module, but honestly I don't know where to go from here.

Is my class Spam not considered part of the module I am trying to import from C++? Is my __init__.py missing something or perhaps I need to include another statement in C++ to import the class after I have imported the module or have I not correctly imported the module in the first place?

I have seen similar questions before but they tended to be in cases where exceptions were raised, the search path was incorrect, or where one the prerequisite values had returned NULL. I am fairly experienced with C++ but am new to Python and newer still to the Python/C API, although I have read the docs covering the basics of the API and how modules work in Python.

GWright
  • 13
  • 3

1 Answers1

1

First, I don't think you need the last two lines in spam.py. I reduced it to this:

class Spam:
    def __init__(self):
        pass
    def print_stuff(self):
        print((1,2,3))

Now, let's test the module:

>>> import spam
>>> s = spam.Spam()
>>> s.print_stuff()
(1, 2, 3)

So far, so good. Let's see if we can use it from C++. Here's a working version of your program:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <stdlib.h>

int main()
{
    Py_Initialize();

    //modify python module search path
    auto modulePath = ".";
    PyObject* sysPath = PySys_GetObject("path");
    PyList_Append(sysPath, PyUnicode_FromString(modulePath));

    PyObject* pModule = PyImport_ImportModule("spam");

    PyObject* spam_class = PyObject_GetAttrString(pModule, "Spam");
    if( !spam_class ) { PyErr_Print(); return EXIT_FAILURE; }

    PyObject* spam = PyObject_CallObject(spam_class, NULL);
    if( !spam ) { PyErr_Print(); return EXIT_FAILURE; }
    
    PyObject* pFunc = PyObject_GetAttrString(spam, "print_stuff");

    if (pFunc != NULL)
        PyObject_CallObject(pFunc, NULL);
    else
        PyErr_Print();
    
    return 1;
}

I named the module spam.py, lowercase. The module object has a Spam class as an attribute. We get that class and call it to create an object, just like we do in Python:

>>> s = spam.Spam()

Now we have an object. It has methods, which are attributes, which we can get with PyObject_GetAttrString, as usual. You know the rest.

It's been a few years since I worked with this stuff, and my experience was from the other side, making C modules accessible to Python. I worked my way to the above example by using dir in the Python interpreter until I got what I needed. If you "think like the interpreter" you may find it all makes more sense.

James K. Lowden
  • 7,574
  • 1
  • 16
  • 31
  • Thank you, this solved the problem but I have a couple of questions. Firstly, I might be missing something but it seems your version of spam.py is the same as mine, did you mean that the __init__.py was unecessary?. Secondly, it seems like my issue was trying to get the function from the module dictionary instead of getting the class from the module and then getting the function from the class. Would I be correct in thinking that member functions of a class are effectively invisible to the containing module? – GWright Apr 06 '21 at 17:22
  • Yes, afaik the init.py is unnecessary. As to "invisible", not exactly. Remember that Python objects don't exist until the class is *executed* by the interpreter. Until then, they're just words on a page. The class definition is lifeless; it defines what an instance will be able to do, but the *instance* methods don't belong to the *class*. It sounds weird to say, but you first execute the class to create an instance. Then you can invoke instance methods. – James K. Lowden Apr 09 '21 at 20:29