使用 Python 扩展 API 包装复杂的 C++ Class
Wrap Complex C++ Class using the Python Extension API
我对创建可以在 Python 中使用的 C++ class 还很陌生。我在互联网上浏览了很多 post。无论是在 Whosebug、gist、github、...我也阅读了文档,但我不确定如何解决我的问题。
基本上,想法是这样做的:http://www.speedupcode.com/c-class-in-python3/
因为我想避免创建自己的 python newtype 的负担,所以我认为像上面的示例那样使用 PyCapsule_New
和 PyCapsule_GetPointer
可能是一种解决方法,但也许我误导了,并且我仍然需要创建复杂的数据类型。
这是我的 class 的 header 我希望能够从 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);
};
所以你可以看到我的 class 包含复杂的结构。 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;
}
在调试我的代码时,我可以看到我的构造函数 return 我的 graphCapsule object 并且它不同于 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
return 编辑的 vgraph.construct
。然后我调用 method1
传递 PyObject pointer
int k = 150
和 bool postprocessing
.
这就是为什么在 *method1
的 C++ 实现中,我使用:
!PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing)
来解析这 3 个 object。
问题是,即使在我调试时,我恢复了 k=150
和 post_processing=False
,它们来自我从 Python 调用 C++ 的方式。 .我也得到一个0X0
,也就是说变量graphCapsule_
...
中的一个nullptr
显然其余代码无法工作...
我认为 PyObject *
是指向我的 graph 类型 Graph<float> *
的指针,所以,我期待 ParseTuple 恢复我的 PyObject *
指针,然后我可以在 PyCapsule_GetPointer
中使用它来检索我的 Object.
如何使我的代码工作?我是否需要定义自己的 PyObject 以便 ParseTuple 理解它?有更简单的方法吗?
非常感谢!
注意:如果我中断我的 python 代码,我可以看到我的图 g
包含一个 PyObject
和地址它指向 object 的名称(这里是 graphtr
),所以我希望我的代码能够工作...
Note2:如果我需要创建自己的 newtype
,我看过这个 Whosebug post: 但我认为是因为我的Class的复杂object,会很难吗?
我回答我自己的问题。
我实际上发现了我的代码中的缺陷。
两个函数 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
类型,所以我应该使用 n
作为 documentation 提到的 I
而不是对 unsigned int 使用 I
:
n (int) [Py_ssize_t]
Convert a Python integer to a C Py_ssize_t.
此外,post_processing
是一个布尔值,因此,即使 documentation 提到可以使用 p
:
解析布尔值
p (bool) [int]
我应该用 int
类型初始化布尔值,而不是 bool
类型,因为它在 Whosebug post
中提到
所以,工作代码是:
size_t k = 300;
int post_processing = true;
if (!PyArg_ParseTuple(args, "O|np", &graphCapsule_, &k, &post_processing)) {
return nullptr;
}
我们也可以通过传递 &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;
}
最后,正如我在问题中提到的,基于这个实现你自己的Python类型其实很简单。我刚刚 copied/pasted 并根据我的需要调整了代码,它就像一个魅力一样工作,不再需要使用 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 提供了一种为 compilation 和 linking 找到推荐标志的方法:
您只需要执行(更改您的 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
标志,因为在调试期间你会得到 [=45= 的提示].为避免这种情况,请从 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/...
我对创建可以在 Python 中使用的 C++ class 还很陌生。我在互联网上浏览了很多 post。无论是在 Whosebug、gist、github、...我也阅读了文档,但我不确定如何解决我的问题。
基本上,想法是这样做的:http://www.speedupcode.com/c-class-in-python3/
因为我想避免创建自己的 python newtype 的负担,所以我认为像上面的示例那样使用 PyCapsule_New
和 PyCapsule_GetPointer
可能是一种解决方法,但也许我误导了,并且我仍然需要创建复杂的数据类型。
这是我的 class 的 header 我希望能够从 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);
};
所以你可以看到我的 class 包含复杂的结构。 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;
}
在调试我的代码时,我可以看到我的构造函数 return 我的 graphCapsule object 并且它不同于 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
return 编辑的 vgraph.construct
。然后我调用 method1
传递 PyObject pointer
int k = 150
和 bool postprocessing
.
这就是为什么在 *method1
的 C++ 实现中,我使用:
!PyArg_ParseTuple(args, "O|Ip", &graphCapsule_, &k, &post_processing)
来解析这 3 个 object。
问题是,即使在我调试时,我恢复了 k=150
和 post_processing=False
,它们来自我从 Python 调用 C++ 的方式。 .我也得到一个0X0
,也就是说变量graphCapsule_
...
nullptr
显然其余代码无法工作...
我认为 PyObject *
是指向我的 graph 类型 Graph<float> *
的指针,所以,我期待 ParseTuple 恢复我的 PyObject *
指针,然后我可以在 PyCapsule_GetPointer
中使用它来检索我的 Object.
如何使我的代码工作?我是否需要定义自己的 PyObject 以便 ParseTuple 理解它?有更简单的方法吗?
非常感谢!
注意:如果我中断我的 python 代码,我可以看到我的图 g
包含一个 PyObject
和地址它指向 object 的名称(这里是 graphtr
),所以我希望我的代码能够工作...
Note2:如果我需要创建自己的 newtype
,我看过这个 Whosebug post:
我回答我自己的问题。
我实际上发现了我的代码中的缺陷。
两个函数 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
类型,所以我应该使用 n
作为 documentation 提到的 I
而不是对 unsigned int 使用 I
:
n (int) [Py_ssize_t]
Convert a Python integer to a C Py_ssize_t.
此外,post_processing
是一个布尔值,因此,即使 documentation 提到可以使用 p
:
p (bool) [int]
我应该用 int
类型初始化布尔值,而不是 bool
类型,因为它在 Whosebug post
所以,工作代码是:
size_t k = 300;
int post_processing = true;
if (!PyArg_ParseTuple(args, "O|np", &graphCapsule_, &k, &post_processing)) {
return nullptr;
}
我们也可以通过传递 &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;
}
最后,正如我在问题中提到的,基于这个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 提供了一种为 compilation 和 linking 找到推荐标志的方法:
您只需要执行(更改您的 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
标志,因为在调试期间你会得到 [=45= 的提示].为避免这种情况,请从 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/...