
系列文章
韓冰:使用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