簡體   English   中英

使用 Python 擴展 API 包裝復雜的 C++ 類

[英]Wrap Complex C++ Class using the Python Extension API

我對創建可以在 Python 中使用的 C++ 類非常陌生。 我瀏覽了互聯網上的很多帖子。 無論是在 StackOverflow、gist、github 上,...我也閱讀了文檔,但我不確定如何解決我的問題。

基本上,這個想法是這樣做的: http : //www.speedupcode.com/c-class-in-python3/因為我想避免創建自己的python PyCapsule_New的負擔,我認為使用PyCapsule_NewPyCapsule_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但邊緣是由定義的結構

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)

這個想法是我保存vgraph.construct返回的PyObject pointer 然后我調用method1傳遞PyObject pointer int k = 150bool postprocessing

這就是為什么在*method1的 C++ 實現中,我使用: !PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing)來解析這 3 個對象。

問題是,即使我在調試時,我恢復了k=150post_processing=False ,它們來自我從 Python 調用 C++ 的方式......我也得到了0X0 ,也就是說變量graphCapsule_nullptr ...

所以顯然其余的代碼無法工作......

我認為PyObject *是一個指向我的類型的曲線Graph<float> * ,因此,我期待ParseTuple恢復我PyObject *指針我然后可以在使用PyCapsule_GetPointer檢索我的對象。

我怎樣才能讓我的代碼工作? 我是否需要定義我自己的 PyObject 以便 ParseTuple 理解它? 有沒有更簡單的方法來做到這一點?

非常感謝!

注意:如果我打破了我的 python 代碼,我可以看到我的圖形g包含一個PyObject及其指向的地址和對象的名稱(這里是graphtr )所以我希望我的代碼能夠工作......

注意2 :如果我需要創建自己的newtype ,我已經看到了這個stackoverflow帖子: How to wrap a C++ object using pure Python Extension API (python3)? 但是我覺得因為我的Class對象比較復雜,會不會比較難?

我回答我自己的問題。

我實際上在我的代碼中發現了這個缺陷。

這兩個函數PyCapsule_GetPointerPyCapsule_New工作得很好。 正如我的問題中提到的,在我嘗試使用以下代碼解析膠囊后,問題就出現了:

size_t k = 300;
bool post_processing = true;

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

問題來自其他參數的解析。 實際上,k 是size_t類型,所以我應該使用n作為文檔中提到的,而不是使用I表示 unsigned int:

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

此外, post_processing是一個布爾值,因此,即使文檔提到可以使用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;
}

最后,正如我的問題中提到的,基於此stackoverflow post實現您自己的 Python 類型實際上很簡單。 我剛剛復制/粘貼並根據我的需要調整了代碼,它就像一個魅力,不再需要使用PyCaspule

其他有用的信息

要調試您的代碼(我在 Linux 上使用了 vscode),您可以使用混合語言調試。 這個想法是將您的 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

你可以看到我全光照-O1 ,而不是-O2-O3

一次,編譯你將有一個.so文件,你可以在 python 中導入和使用。 對於我的示例,我將使用vgraph.so並且在我的 python 代碼中,我可以執行以下操作:

import vgraph.so

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

然后您就可以輕松調試 C++。 互聯網上有一些帖子解釋了如何使用 vs code/gdb/...

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM