Обернуть сложный класс C ++ с помощью API-интерфейса расширения Python - PullRequest
2 голосов
/ 13 февраля 2020

Я довольно новичок в создании класса C ++, который я могу использовать из Python. Я просмотрел много постов на inte rnet. Будь то на StackOverflow, GIST, GitHub, ... Я также прочитал документацию, но я не уверен, как я могу решить мою проблему.

По сути, идея состоит в том, чтобы сделать это: http://www.speedupcode.com/c-class-in-python3/ Поскольку я хочу избежать бремени создания своего собственного python newtype , я подумал, что использование PyCapsule_New и PyCapsule_GetPointer, как в примере выше, может быть обходным решением, но, возможно, я ввел в заблуждение, и мне все еще нужно создать сложный тип данных.

Вот заголовок моего класса, из которого я хочу позвонить с 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);
};

Итак, как вы можете видеть, мой класс содержит сложные структуры. vector1D - это просто std::vector, но edge - это структура, определяемая

template<typename T>
struct edge {
    tuple2 src;
    tuple2 dst;
    T weight;
};

, а некоторые методы используют другие сложные структуры.

В любом случае, я создал собственную привязку Python. Здесь я только поставил соответствующие функции. Я создал constructor следующим образом:

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;
}

Во время отладки моего кода я вижу, что мой конструктор возвращает мой объект graphCapsule, и тогда он отличается от nullptr.

. Я создаю свою method1 функцию следующим образом:

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;
}

Когда я все скомпилирую, у меня будет библиотека vgraph.so, и я буду вызывать ее из Python, используя:

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)

Идея состоит в том, что я сохраняю PyObject pointer, возвращаемый vgraph.construct. Затем я вызываю method1, передавая PyObject pointer int k = 150 и bool postprocessing.

. Поэтому в реализации C ++ *method1 я использую: !PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing) для анализа этих 3 объектов. .

Проблема в том, что, когда я отлаживаю, я восстанавливаю k=150 и post_processing=False, которые исходят из того, как я вызываю C ++ из Python ... Я также получая 0X0, то есть nullptr в переменной graphCapsule_ ...

Так что, очевидно, остальная часть кода не может работать ...

Я думал что PyObject * является указателем на мой граф типа Graph<float> *, поэтому я ожидал, что ParseTuple восстановит мой PyObject * указатель, который я затем смогу использовать в PyCapsule_GetPointer для получения моего объекта.

Как мне заставить мой код работать? Нужно ли мне определять свой собственный PyObject, чтобы ParseTuple понимал его? Есть ли более простой способ сделать это?

Большое спасибо!

Примечание : если я нарушу свой код python, я вижу, что мой график g содержит PyObject с адресом, на который он указывает, и именем объекта (здесь graphtr), поэтому я ожидал, что мой код заработает ...

Note2 : Если мне нужно создать свой собственный newtype, я видел следующее сообщение: Как обернуть объект C ++ с помощью чистого Python API-расширения (python3)? , но я думаю из-за сложного объекты моего класса, это будет довольно сложно?

1 Ответ

0 голосов
/ 18 февраля 2020

Я отвечаю на свой вопрос.

Я действительно обнаружил недостаток в своем коде.

Обе функции PyCapsule_GetPointer и PyCapsule_New работают отлично. Как уже упоминалось в моем вопросе, проблема возникла сразу после того, как я пытался разобрать капсулу с помощью следующего кода:

size_t k = 300;
bool post_processing = true;

if (!PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing)) {
    return nullptr;
}

Проблема связана с анализом других параметров. Действительно, k является типом size_t, поэтому вместо использования I для unsigned int я должен использовать n в качестве документации :

n (int) [Py_ssize_t]
Convert a Python integer to a C Py_ssize_t.

Более того, post_processing является логическим значением, поэтому, хотя в документации 1020 * упоминается, что логическое значение может быть проанализировано с p:

p (bool) [int]

, я должен инициализировать логическое значение с типом int вместо введите bool, как указано в этом сообщении stackoverflow

Итак, рабочий фрагмент кода:

size_t k = 300;
int post_processing = true;

if (!PyArg_ParseTuple(args, "O|np", &graphCapsule_, &k, &post_processing)) {
    return nullptr;
}

Мы также можем использовать O! опций, передав &Pycapsule_Type:

#include <pycapsule.h>
...
size_t k = 300;
int post_processing = true;

if (!PyArg_ParseTuple(args, "O!|np", &PyCapsule_Type, &graphCapsule_, &k, &post_processing)) {
    return nullptr;
}

Наконец, как уже упоминалось в моем вопросе, на самом деле очень просто реализовать свой собственный тип Python на основе этого сообщения stackoverflow . Я только что скопировал / вставил и адаптировал код под свои нужды, и он работает как брелок без необходимости использовать PyCaspule больше!

Другая полезная информация :

Для отладки вашего кода (я использовал vscode на Linux), вы можете использовать отладку на разных языках. Идея состоит в том, чтобы скомпилировать ваш код C ++ в общую библиотеку .so.

После компиляции кода вы можете импортировать его в python:

import my_lib

где my_lib ссылается на файл my_lib.so, который вы создали.

Чтобы создать файл .so, вам просто нужно выполнить: g++ my_python_to_cpp_wrapper.cpp --ggdb -o my_python_to_cpp_wrapper.so

Но, если вы сделаете это, вы можете пропущено включение библиотеки python и прочего ...

К счастью, python предоставляет способ найти рекомендуемые флаги для компиляции и , связывающей :

вам просто нужно выполнить (измените python версию или, в конце концов, загляните в / usr / local / bin)

/usr/bin/python3.6m-config --cflags

Для меня он вернул:

-I/usr/include/python3.6m -I/usr/include/python3.6m  -Wno-unused-result -Wsign-compare -g -fdebug-prefix-map=/build/python3.6-0aiVHW/python3.6-3.6.9=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector -Wformat -Werror=format-security  -DNDEBUG -g -fwrapv -O3 -Wall

То же самое заявление о связывании (измените версию python или, в конце концов, загляните в / usr / local / bin)

/usr/bin/python3.6m-config --ldflags

дало мне:

-L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl  -lutil -lm  -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions

Тогда, так как мы хотим создать разделяемую библиотеку .so, нам нужно добавить флаг -shared, а также флаги -fPIC (иначе он будет жаловаться). Наконец, так как мы хотим отладить наш код, мы должны удалить все -Ox, такие как -O2 или -O3 флаг, который оптимизирует код, потому что во время отладки вам будет предложено ввести <optimized out>. Чтобы избежать этого, удалите все флаги оптимизации из опций g++. Например, в моем случае мой cpp файл называется: vrgaph. cpp и вот как я его скомпилировал:

g++ vgraph.cpp -ggdb -o vgraph.so -I/usr/include/python3.6m -I/usr/include/python3.6m  -Wno-unused-result -Wsign-compare -g -fdebug-prefix-map=/build/python3.6-0aiVHW/python3.6-3.6.9=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector -Wformat -Werror=format-security  -DNDEBUG -g -fwrapv -Wall -L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl  -lutil -lm  -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions -shared -fPIC

You можно увидеть, что я использую -O1 вместо -O2 или -O3.

После компиляции у вас будет файл .so, который можно импортировать и использовать в python. Для моего примера у меня будет vgraph.so, и в моем коде python я могу сделать:

import vgraph.so

# rest of the call that use you C++ backend code

Тогда вы можете легко отладить ваш C ++. В inte * есть несколько постов rnet, в которых объясняется, как это сделать с vs code / gdb /...

...