I'm pretty new to creating C++ class that I can use from within Python. I've skimmed through a lot of posts on the internet. Be it on StackOverflow, gist, github, ... I've also read the documentation, but I'm not sure how I can solve my issue.
Basically, the idea is to do this: http://www.speedupcode.com/c-class-in-python3/
As I want to avoid the burden of creating my own python newtype, I thought that using PyCapsule_New
and PyCapsule_GetPointer
as in the example above could be a workaround, but maybe I'm misleading, and I still need to create complex datatype.
Here is the header of my class I want to be able to call from python:
template<typename T>
class Graph {
public:
Graph(const vector3D<T>& image, const std::string& similarity, size_t d) : img(image) {...}
component<T> method1(const int k, const bool post_processing=true);
private:
caller_map<T> cmap;
vector3D<T> img; // input image with 3 channels
caller<T> sim; // similarity function
size_t h; // height of the image
size_t w; // width of the image
size_t n_vertices; // number of pixels in the input image
size_t conn; // radius for the number of connected pixels
vector1D<edge<T>> edges; // graph = vector of edges
void create_graph(size_t d);
tuple2 find(vector2D<subset>& subsets, tuple2 i);
void unite(vector2D<subset>& subsets, tuple2 x, tuple2 y);
};
So As you can see my class contains complex structures. vector1D
is just std::vector
but edge is a structure defined by
template<typename T>
struct edge {
tuple2 src;
tuple2 dst;
T weight;
};
and some methods use other complex structures.
Anyway, I have created my own Python binding. Here I only put the relevant functions. I created my constructor
as follow:
static PyObject *construct(PyObject *self, PyObject *args, PyObject *kwargs) {
// Arguments passed from Python
PyArrayObject* arr = nullptr;
// Default if arguments not given
const char* sim = "2000"; // similarity function used
const size_t conn = 1; // Number of neighbor pixels to consider
char *keywords[] = {
"image",
"similarity",
"d",
nullptr
};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&|sI:vGraph", keywords, PyArray_Converter, &arr, &sim, &conn)) {
// Will need to DECRF(arr) somewhere?
return nullptr;
}
set<string> sim_strings = {"1976", "1994", "2000"};
if (sim_strings.find(sim) == sim_strings.end()) {
PyErr_SetString(PyExc_ValueError, "This similarity function does not exist");
Py_RETURN_NONE;
}
// Parse the 3D numpy array to vector3D
vector3D<float> img = parse_PyArrayFloat<float>(arr);
// call the Constructor
Graph<float>* graph = new Graph<float>(img, sim, conn);
// Create Python capsule with a pointer to the `Graph` object
PyObject* graphCapsule = PyCapsule_New((void * ) graph, "graphptr", vgraph_destructor);
// int success = PyCapsule_SetPointer(graphCapsule, (void *)graph);
// Return the Python capsule with the pointer to `Graph` object
// return Py_BuildValue("O", graphCapsule);
return graphCapsule;
}
While debugging my code, I can see that my constructor return my graphCapsule object and that it is different from nullptr
.
then I create my method1
function as follow:
static PyObject *method1(PyObject *self, PyObject *args) {
// Capsule with the pointer to `Graph` object
PyObject* graphCapsule_;
// Default parameters of the method1 function
size_t k = 300;
bool post_processing = true;
if (!PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing)) {
return nullptr;
}
// Get the pointer to `Graph` object
Graph<float>* graph = reinterpret_cast<Graph<float>* >(PyCapsule_GetPointer(graphCapsule_, "graphptr"));
// Call method1
component<float> ctov = graph->method1(k, post_processing);
// Convert component<float> to a Python dict (bad because we need to copy?)
PyObject* result = parse_component<float>(ctov);
return result;
}
When I compile everything, I will have a vgraph.so
library and I will call it from Python using:
import vgraph
import numpy as np
import scipy.misc
class Vgraph():
def __init__(self, img, similarity, d):
self.graphCapsule = vgraph.construct(img, similarity, d)
def method1(self, k=150, post_processing=True):
vgraph.method1(self.graphCapsule, k, post_processing)
if __name__ == "__main__":
img = scipy.misc.imread("pic.jpg")
img = scipy.misc.imresize(img, (512, 512)) / 255
g = Vgraph(lab_img, "1976", d=1)
cc = g.method1(k=150, post_processing=False)
The idea is that I save the PyObject pointer
returned by the vgraph.construct
. Then I call method1
passing the PyObject pointer
the int k = 150
and the bool postprocessing
.
This is why in the C++ implementation of *method1
, I use:
!PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing)
to parse these 3 objects.
The problem is, even though, when I'm debugging, I recover k=150
and post_processing=False
which come from the way I'm calling the C++ from Python... I'm also getting a 0X0
, that is to say a nullptr
in the variable graphCapsule_
...
So obviously the rest of the code cannot work...
I thought that PyObject *
is a pointer to my graph of type Graph<float> *
, so, I was expecting ParseTuple to recover my PyObject *
pointer that I can then use in PyCapsule_GetPointer
to retrieve my Object.
How can I make my code work? Do I need to define my own PyObject so that ParseTuple understand it? Is there a simpler way to do it?
Thanks a lot!
Note: If I break in my python code, I can see that my graph g
contains a PyObject
with the address it points to and the name of the object (here graphtr
) so I was expecting my code to work...
Note2: If I need to create my own newtype
, I have seen this stackoverflow post: How to wrap a C++ object using pure Python Extension API (python3)? but I think because of the complex objects of my Class, it will be quite difficult?