
系列文章
韩冰:使用c/c++编写python扩展(一):定义模块函数与异常zhuanlan.zhihu.com
韩冰:使用c/c++编写python扩展(二)- Vscode Python C++联合调试zhuanlan.zhihu.com
本文继续介绍使用c/c++编写python扩展。上文介绍了定义Python模块,函数与异常,并且介绍了如何编写setup.py构建和打包。Python 运行时将所有 Python 对象都视为类型 PyObject* 的变量,即所有 Python 对象的"基础类型"。 PyObject 结构体本身包含了对象的 reference count 和对象的"类型对象"。 类型对象确定解释器需要调用哪些 (C) 函数,例如一个属性查询一个对象,一个方法调用,或者与另一个对象相乘。 这些 C 函数被称为“类型方法”。Python中的内置类型,如list, dict,int,str等类型都是通过这种方法进行定义的。这类事情只能用例子解释本文通过定义一个Matrix类型,并重载加减乘除等运算符扩展Python的内置类型。(本文使用C++进行演示)
目录
- 功能展示
- Eigen简介
- 定义一个空的Matrix类型
- 对象的创建与销毁
- 重载+,-,*,[]等运算符
- 添加对象属性
- 添加方法
功能展示
我们的目标是定义一个matrix模块,该模块中有一些创建矩阵的方法,我们可以对创建的矩阵进行常用的运算,并访问它们的一些属性。
import
Eigen简介
为了简单起见,我们选择使用开源的矩阵库进行c++端的矩阵运算,下面我们演示在C++中使用Eigen如何实现上述功能。最终我们会将Eigen中的Matrix类型封装成可供Python调用的数据类型。
#include
定义一个空的Matrix类型
使用C++定义Python的新类型需要定义两个数据结构,一个是类类型数据结构,一个是对象类型数据结构。类类型数据结构中主要定义了该类型对象的方法、静态属性、属性访问方法等,对应于python中class的定义。而对象类型则是每创建一个类型实例(即对象)都必须创建的结构体,数据结构中包含了每个对象的对象属性。
首先是对象类型,每个对象类型必须包含PyObject_HEAD头部,它定义在Python.h文件中,相当于python中object类型的定义,所有Python对象类型结构体都必须继承该结构。接下来就是我们自定义的属性,在本例中是一个指向eigen矩阵对象的一个指针。
typedef
接下来是类类型,与上述是一个新定义的结构体不同,它是一个PyTypeObject类型的实例。PyTypeObject结构体有相当多的字段,需要我们根据需求进行定义,下面的代码列出了它的所有字段(每个字段的详细介绍可以参考官方文档)
Type Objects - Python 3.8.2 documentationdocs.python.org
已经填充的字段是我们需要用到的,每个填充的字段后文会分别介绍。对于用不到的字段都填充为空指针。
static PyTypeObject MatrixType = {
PyVarObject_HEAD_INIT(nullptr, 0)
"matrix.Matrix", /* tp_name */
sizeof(PyMatrixObject), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)PyMatrix_dealloc, /* tp_dealloc */
nullptr, /* tp_print */
nullptr, /* tp_getattr */
nullptr, /* tp_setattr */
nullptr, /* tp_reserved */
nullptr, /* tp_repr */
&numberMethods, /* tp_as_number */
nullptr, /* tp_as_sequence */
nullptr, /* tp_as_mapping */
nullptr, /* tp_hash */
nullptr, /* tp_call */
PyMatrix_str, /* tp_str */
nullptr, /* tp_getattro */
nullptr, /* tp_setattro */
nullptr, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
"Coustom matrix class.", /* tp_doc */
nullptr, /* tp_traverse */
nullptr, /* tp_clear */
nullptr, /* tp_richcompare */
0, /* tp_weaklistoffset */
nullptr, /* tp_iter */
nullptr, /* tp_iternext */
MatrixMethods, /* tp_methods */
nullptr, /* tp_members */
MatrixGetSet, /* tp_getset */
nullptr, /* tp_base */
nullptr, /* tp_dict */
nullptr, /* tp_descr_get */
nullptr, /* tp_descr_set */
0, /* tp_dictoffset */
nullptr, /* tp_init */
nullptr, /* tp_alloc */
PyMatrix_new /* tp_new */
};
其中
PyVarObject_HEAD_INIT(nullptr, 0)
是固定的写法,与上文提到的PyObject_HEAD的初始化相关,这里先不做深入的介绍。
"matrix.Matrix", /* tp_name */
tp_name字段告诉我们的Python解释器我们可以通过
import matrix
matrix.Matrix
的方式访问到Matrix类型。这里很重要的一点是 "." 前面一定要与我们的模块名一致,“.”后面是类型的名字。如果随意定义的话会导致解释器在某些情况下的异常行为,如使用pickle模块进行序列化与反序列化时。
sizeof(PyMatrixObject), /* tp_basicsize */
tp_basicsize字段告诉解释器每一个Matrix对象占据的内存大小,很明显这里是我们对象类型每个实例所占据的内存大小。
至此我们已经完成了定义一个新类型的最简单的操作,接下来我们只需要创建一个模块,将新类型引入到模块中,就可以让Python解释器进行加载了。
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"matrix",
"Python interface for Matrix calculation",
-1,
matrixMethods};
PyObject *initModule(void)
{
PyObject *m;
if (PyType_Ready(&MatrixType) < 0)
return NULL;
m = PyModule_Create(&module);
if (m == NULL)
return NULL;
Py_INCREF(&MatrixType);
if (PyModule_AddObject(m, "Matrix", (PyObject *)&MatrixType) < 0)
{
Py_DECREF(&MatrixType);
Py_DECREF(m);
return NULL;
}
return m;
}
模块定义的部分我们在
韩冰:使用c/c++编写python扩展(一):定义模块函数与异常zhuanlan.zhihu.com
章节中有详细的讲解,在之前代码的基础上,我们将自定义的Matrix类型加入到了模块中
if (PyModule_AddObject(m, "Matrix", (PyObject *)&MatrixType) < 0)
{
Py_DECREF(&MatrixType);
Py_DECREF(m);
return NULL;
}
PyModule_AddObject需要三个参数,分别是模块指针,类型名称,类型类类型指针。如果创建失败,则将模块和类类型的引用计数清零并返回空指针,意为创建失败。有关Python中引用计数以及垃圾回收的话题我们会在后续的文章中讲到,这里暂时不做赘述。
对象的创建与销毁(构造函数与析构函数)
在Python中,对象的创建和初始化与两个方法有关,一个是_new方法一个是__init__方法
class SomeClass(object):
def __init__(self, *args, **kwargs):
# do some you want
pass
def __new__(cls, *args, **kwargs):
# do some you want
obj = ....
return obj
这里我们选择在_new_方法中初始化Eigen矩阵指针。
static
可以看出,该函数的三个参数对应于_new_方法的三个参数。我们首先从PyTypeObject对象中创建我们的PyObject,这里的type传入的就是我们刚才定义的类类型MatrixType。
PyMatrixObject *self;
self = (PyMatrixObject *)type->tp_alloc(type, 0);
这种写法是固定的,tp_alloc方法为我们创建新的对象,由于返回的是基类型对象指针,所以我们先将指针强制转换成PyMatrixObject类型。目的是访问PyMatrixObject的matrix指针并为其赋值。
self->matrix = new MatrixXd(width, height);
return (PyObject *)self;
最后我们在把self转换为PyObject类型返回,至此我们新的对象就创建完毕了。函数定义完毕后,我们就可以将MatrixType的tp_new字段指定为PyMatrix_new函数指针。
PyMatrix_new /* tp_new */
我们在完成对象的创建后要考虑一个问题,Python是采用引用计数的方式进行垃圾回收的,当我们创建的对象计数为0时,我们的Python解释器会自动帮我们释放创建的对象的内存,也就是 PyMatrixObject 结构体的实例。但是 PyMatrixObject 实例中只包含了我们Matrix的指针,而Matrix指向的对象堆内存并没有得到释放,这就会造成内存的泄露。因此我们需要实现tp_dealloc进行内存的释放。
static void
*PyMatrix_dealloc(PyObject *obj)
{
delete ((PyMatrixObject *)obj)->matrix;
Py_TYPE(obj)->tp_free(obj);
}
这个函数实现了matrix指针指向的内存的释放,同时也释放了Python对象。值得注意的是,及时我们不实现该函数,Python解释器也会调用 Py_TYPE(obj)->tp_free(obj);,复写时我们除了实现额外操作外,也必须将这条语句加上。
重载运算符
我们常见的一些python库如numpy,pytorch这些都支持自定义类型的加减乘除等运算,这些事如何实现的呢。答案是实现tp_as_number字段。正如它的名字一样,实现像”数字“一样的运算方式。tp_as_number字段我们可以这样定义它:
static
其中nb_add, nb_substract, nb_multiply是我们想要重载的三个运算符,其他运算符所代表的含义我们也可以在官方文档中找到。我们以乘法为例距离说明:
static
熟悉c++运算符重载机制的应该都很容易看懂,由于"
" 号是一个双目运算符,所以函数接收两个参数,分别是 "*"号两边的两个对象。运算结束后,将运算结果封装成Python对象返回。
添加对象属性
我们希望通过 a.row, a.column 来访问矩阵的行数和列数改如何做呢。我们需要定义tp_getset字段。这个字段定义如下
PyObject
这种方法相当于python中的@property。
添加类的成员方法
添加成员方法类似于我们在定义模块函数与异常一文中提到的在模块中定义方法,这里不再赘述,直接上代码
PyObject
同样的,我们需要定义几个模块级别的方法,用做矩阵的初始化
static
Putting it together!
下面给出完整的代码
http://python_matrix.cc
#include <Python.h>
#include <iostream>
#include <Eigen/Dense>
using namespace Eigen;
typedef struct
{
PyObject_HEAD
MatrixXd *matrix=nullptr;
} PyMatrixObject;
static PyObject *
PyMatrix_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyMatrixObject *self;
self = (PyMatrixObject *)type->tp_alloc(type, 0);
char *kwlist[] = {"width", "height", NULL};
int width = 0;
int height = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", kwlist,
&width, &height))
{
Py_DECREF(self);
return NULL;
}
if (width <= 0 or height <= 0)
{
PyErr_SetString(PyExc_ValueError, "The height and width must be greater than 0.");
return NULL;
}
self->matrix = new MatrixXd(width, height);
return (PyObject *)self;
}
static void
*PyMatrix_dealloc(PyObject *obj)
{
delete ((PyMatrixObject *)obj)->matrix;
Py_TYPE(obj)->tp_free(obj);
}
inline MatrixXd *ParseMatrix(PyObject *obj){
return ((PyMatrixObject *)obj)->matrix;
}
inline PyObject *ReturnMatrix(MatrixXd *m, PyTypeObject *type){
PyMatrixObject *obj = PyObject_NEW(PyMatrixObject, type);
obj->matrix = m;
return (PyObject *)obj;
}
static PyObject *
PyMatrix_add(PyObject *a, PyObject *b)
{
MatrixXd *matrix_a = ParseMatrix(a);
MatrixXd *matrix_b = ParseMatrix(b);
if (matrix_a->cols() != matrix_b->cols() or matrix_a->rows() != matrix_b->rows()){
PyErr_SetString(PyExc_ValueError, "The input matrix must be the same shape.");
return NULL;
}
MatrixXd *matrix_c = new MatrixXd(matrix_a->cols(), matrix_b->rows());
*matrix_c = *matrix_a + *matrix_b;
return ReturnMatrix(matrix_c, a->ob_type);
}
static PyObject *
PyMatrix_minus(PyObject *a, PyObject *b)
{
MatrixXd *matrix_a = ParseMatrix(a);
MatrixXd *matrix_b = ParseMatrix(b);
if (matrix_a->cols() != matrix_b->cols() or matrix_a->rows() != matrix_b->rows()){
PyErr_SetString(PyExc_ValueError, "The input matrix must be the same shape.");
return NULL;
}
MatrixXd *matrix_c = new MatrixXd(matrix_a->cols(), matrix_b->rows());
*matrix_c = *matrix_a + *matrix_b;
return ReturnMatrix(matrix_c, a->ob_type);
}
static PyObject *
PyMatrix_multiply(PyObject *a, PyObject *b)
{
MatrixXd *matrix_a = ParseMatrix(a);
MatrixXd *matrix_b = ParseMatrix(b);
if (matrix_a->cols() != matrix_b->rows()){
PyErr_SetString(PyExc_ValueError, "The colonm rank of matrix A must be the same as the row rank of matrix B.");
return NULL;
}
MatrixXd *matrix_c = new MatrixXd(matrix_a->rows(), matrix_b->cols());
*matrix_c = (*matrix_a) * (*matrix_b);
return ReturnMatrix(matrix_c, a->ob_type);
}
static PyObject *PyMatrix_str(PyObject *a)
{
MatrixXd *matrix = ParseMatrix(a);
std::stringstream ss;
ss << *matrix;
return Py_BuildValue("s", ss.str().c_str());
}
static PyNumberMethods numberMethods = {
PyMatrix_add, //nb_add
PyMatrix_minus, //nb_subtract;
PyMatrix_multiply, //nb_multiply
nullptr, //nb_remainder;
nullptr, //nb_divmod;
nullptr, // nb_power;
nullptr, // nb_negative;
nullptr, // nb_positive;
nullptr, // nb_absolute;
nullptr, // nb_bool;
nullptr, // nb_invert;
nullptr, // nb_lshift;
nullptr, // nb_rshift;
nullptr, // nb_and;
nullptr, // nb_xor;
nullptr, // nb_or;
nullptr, // nb_int;
nullptr, // nb_reserved;
nullptr, // nb_float;
nullptr, // nb_inplace_add;
nullptr, // nb_inplace_subtract;
nullptr, // nb_inplace_multiply;
nullptr, // nb_inplace_remainder;
nullptr, // nb_inplace_power;
nullptr, // nb_inplace_lshift;
nullptr, // nb_inplace_rshift;
nullptr, // nb_inplace_and;
nullptr, // nb_inplace_xor;
nullptr, // nb_inplace_or;
nullptr, // nb_floor_divide;
nullptr, // nb_true_divide;
nullptr, // nb_inplace_floor_divide;
nullptr, // nb_inplace_true_divide;
nullptr, // nb_index;
nullptr, //nb_matrix_multiply;
nullptr //nb_inplace_matrix_multiply;
};
PyObject *PyMatrix_data(PyObject *self, void *closure)
{
PyMatrixObject *obj = (PyMatrixObject *)self;
Py_ssize_t width = obj->matrix->cols();
Py_ssize_t height = obj->matrix->rows();
PyObject *list = PyList_New(height);
for (int i = 0; i < height; i++)
{
PyObject *internal = PyList_New(width);
for (int j = 0; j < width; j++)
{
PyObject *value = PyFloat_FromDouble((*obj->matrix)(i, j));
PyList_SetItem(internal, j, value);
}
PyList_SetItem(list, i, internal);
}
return list;
}
PyObject *PyMatrix_rows(PyObject *self, void *closure)
{
PyMatrixObject *obj = (PyMatrixObject *)self;
return Py_BuildValue("i", obj->matrix->rows());
}
PyObject *PyMatrix_cols(PyObject *self, void *closure)
{
PyMatrixObject *obj = (PyMatrixObject *)self;
return Py_BuildValue("i", obj->matrix->cols());
}
static PyGetSetDef MatrixGetSet[] = {
{"data", (getter)PyMatrix_data, nullptr, nullptr},
{"row", (getter)PyMatrix_rows, nullptr, nullptr},
{"colunm", (getter)PyMatrix_cols, nullptr, nullptr},
{nullptr}};
PyObject *PyMatrix_tolist(PyObject *self, PyObject *args)
{
return PyMatrix_data(self, nullptr);
}
static PyMethodDef MatrixMethods[] = {
{"to_list", (PyCFunction)PyMatrix_tolist, METH_VARARGS, "Return the matrix data to a list object."},
{nullptr}};
static PyTypeObject MatrixType = {
PyVarObject_HEAD_INIT(nullptr, 0) "matrix.Matrix", /* tp_name */
sizeof(PyMatrixObject), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)PyMatrix_dealloc, /* tp_dealloc */
nullptr, /* tp_print */
nullptr, /* tp_getattr */
nullptr, /* tp_setattr */
nullptr, /* tp_reserved */
nullptr, /* tp_repr */
&numberMethods, /* tp_as_number */
nullptr, /* tp_as_sequence */
nullptr, /* tp_as_mapping */
nullptr, /* tp_hash */
nullptr, /* tp_call */
PyMatrix_str, /* tp_str */
nullptr, /* tp_getattro */
nullptr, /* tp_setattro */
nullptr, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
"Coustom matrix class.", /* tp_doc */
nullptr, /* tp_traverse */
nullptr, /* tp_clear */
nullptr, /* tp_richcompare */
0, /* tp_weaklistoffset */
nullptr, /* tp_iter */
nullptr, /* tp_iternext */
MatrixMethods, /* tp_methods */
nullptr, /* tp_members */
MatrixGetSet, /* tp_getset */
nullptr, /* tp_base */
nullptr, /* tp_dict */
nullptr, /* tp_descr_get */
nullptr, /* tp_descr_set */
0, /* tp_dictoffset */
nullptr, /* tp_init */
nullptr, /* tp_alloc */
PyMatrix_new /* tp_new */
};
static PyObject *PyMatrix_ones(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyMatrixObject *m = (PyMatrixObject *)PyMatrix_new(&MatrixType, args, kwargs);
m->matrix->setOnes();
return (PyObject *)m;
}
static PyObject *PyMatrix_zeros(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyMatrixObject *m = (PyMatrixObject *)PyMatrix_new(&MatrixType, args, kwargs);
m->matrix->setZero();
return (PyObject *)m;
}
static PyObject *PyMatrix_random(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyMatrixObject *m = (PyMatrixObject *)PyMatrix_new(&MatrixType, args, kwargs);
m->matrix->setRandom();
return (PyObject *)m;
}
static PyObject *PyMatrix_matrix(PyObject *self, PyObject *args)
{
PyObject *data = nullptr;
if (!PyArg_ParseTuple(args, "O", &data))
{
PyErr_SetString(PyExc_ValueError, "Please pass a 2 dimensions list object. 1");
return nullptr;
}
if (!PyList_Check(data))
{
PyErr_SetString(PyExc_ValueError, "Please pass a 2 dimensions list object. 2");
return nullptr;
}
int height = PyList_GET_SIZE(data);
if (height <= 0)
{
PyErr_SetString(PyExc_ValueError, "Please pass a 2 dimensions list object. 2");
return nullptr;
}
PyObject *list = PyList_GET_ITEM(data, 0);
if (!PyList_Check(list))
{
PyErr_SetString(PyExc_ValueError, "Please pass a 2 dimensions list object. 3");
return nullptr;
}
int width = PyList_GET_SIZE(list);
MatrixXd *p_mat = new MatrixXd(width, height);
for (int i = 0; i < height; i++)
{
PyObject *list = PyList_GET_ITEM(data, i);
if (!PyList_Check(list))
{
PyErr_SetString(PyExc_ValueError, "Please pass a 2 dimensions list object. 3");
return nullptr;
}
int tmp = PyList_GET_SIZE(list);
if (width != tmp)
{
PyErr_SetString(PyExc_ValueError, "Please pass a 2 dimensions list object. Each elements of it must be the same length.");
return nullptr;
}
width = tmp;
for (int j = 0; j < width; j++)
{
PyObject *num = PyList_GET_ITEM(list, j);
if (!PyFloat_Check(num))
{
PyErr_SetString(PyExc_ValueError, "Every elements of the matrix must float.");
return nullptr;
}
(*p_mat)(i, j) = ((PyFloatObject *)num)->ob_fval;
}
}
return ReturnMatrix(p_mat, &MatrixType);
}
static PyMethodDef matrixMethods[] = {
{"ones", (PyCFunction)PyMatrix_ones, METH_VARARGS | METH_KEYWORDS, "Return a new matrix with initial values one."},
{"zeros", (PyCFunction)PyMatrix_zeros, METH_VARARGS | METH_KEYWORDS, "Return a new matrix with initial values zero."},
{"random", (PyCFunction)PyMatrix_random, METH_VARARGS | METH_KEYWORDS, "Return a new matrix with random values"},
{"matrix", (PyCFunction)PyMatrix_matrix, METH_VARARGS, "Return a new matrix with given values"},
{nullptr}};
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"matrix",
"Python interface for Matrix calculation",
-1,
matrixMethods};
PyObject *initModule(void)
{
PyObject *m;
if (PyType_Ready(&MatrixType) < 0)
return NULL;
m = PyModule_Create(&module);
if (m == NULL)
return NULL;
Py_INCREF(&MatrixType);
if (PyModule_AddObject(m, "Matrix", (PyObject *)&MatrixType) < 0)
{
Py_DECREF(&MatrixType);
Py_DECREF(m);
return NULL;
}
return m;
}
CMakeLists.txt
cmake_minimum_required
stub.cc
#define PY_SSIZE_T_CLEAN
#include <Python.h>
extern PyObject* initModule();
PyMODINIT_FUNC
PyInit_matrix(void)
{
return initModule();
}
setup.py
import
最后解释一下,我们单独新建了一个http://stub.cc文件作为Extention模块的入口文件,将初始化的函数声明为extern,这样我们就可以使用构建工具如Cmake对源代码进行编译,在setup.py中只需要引入编译好的静态库和动态库,并把http://stub.cc作为入口文件引入”source”字段即可。这也是Pytorch源码中的做法。
另外本章节的所有代码也可以在我的github中找到
https://github.com/BrightXiaoHan/CMakeTutorial/tree/master/PythonExtentiongithub.com