【问题标题】:Wrap Complex C++ Class using the Python Extension API使用 Python 扩展 API 包装复杂的 C++ 类
【发布时间】:2020-02-13 10:09:37
【问题描述】:

我对创建可以在 Python 中使用的 C++ 类非常陌生。我在网上浏览了很多帖子。无论是在 StackOverflow、gist、github 上,......我也阅读了文档,但我不确定如何解决我的问题。

基本上,这个想法是这样做的:http://www.speedupcode.com/c-class-in-python3/ 由于我想避免创建自己的python newtype 的负担,我认为使用上面示例中的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 但 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)

这个想法是我保存vgraph.construct返回的PyObject pointer。然后我调用method1 传递PyObject pointer int k = 150bool postprocessing

这就是为什么在 *method1 的 C++ 实现中,我使用: !PyArg_ParseTuple(args, "O|Ip", &amp;graphCapsule_, &amp;k, &amp;post_processing)解析这3个对象。

问题是,尽管在调试时,我恢复了 k=150post_processing=False,它们来自我从 Python 调用 C++ 的方式......我也得到了 0X0 ,也就是说变量graphCapsule_中的一个nullptr...

所以很明显其余的代码不能工作......

我认为PyObject * 是指向我的Graph&lt;float&gt; * 类型的 的指针,所以,我期待ParseTuple 恢复我的PyObject * 指针,然后我可以在PyCapsule_GetPointer 中使用它检索我的对象。

我怎样才能使我的代码工作?我是否需要定义自己的 PyObject 以便 ParseTuple 理解它?有没有更简单的方法?

非常感谢!

注意:如果我中断了我的 python 代码,我可以看到我的图形 g 包含一个 PyObject 及其指向的地址和对象的名称(这里是 @987654356 @) 所以我期待我的代码能够工作......

注意2:如果我需要创建自己的newtype,我看过这个stackoverflow帖子:How to wrap a C++ object using pure Python Extension API (python3)?,但我认为由于我的类的复杂对象,这将是相当困难的?

【问题讨论】:

    标签: python c++ class binding wrapper


    【解决方案1】:

    我回答我自己的问题。

    我实际上发现了我的代码中的缺陷。

    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 作为无符号整数,因为documentation 提到:

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

    此外,post_processing 是一个布尔值,所以即使documentation 提到可以用p 解析一个布尔值:

    p (bool) [int]
    

    我应该使用int 类型而不是bool 类型来初始化布尔值,正如stackoverflow post 中提到的那样

    所以,工作代码是:

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

    我们也可以通过&amp;Pycapsule_Type 来使用O! 选项:

    #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 标志,因为在调试期间您会收到&lt;optimized out&gt; 的提示。为避免这种情况,请从您的 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/...

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-13
      相关资源
      最近更新 更多