GWright
GWright

Reputation: 13

Why am I unable to access my Python function after importing its module in C++?

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.

Upvotes: 1

Views: 678

Answers (1)

James K. Lowden
James K. Lowden

Reputation: 7837

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.

Upvotes: 1

Related Questions