Windows下Python3調用C++函數

  • 本博客運行環境爲Windows 10 + VS2015 + python3.6;
  • 主要流程爲將.cpp文件封裝成.dll文件,改名爲.pyd後可以直接在python下import
  • 更多詳細信息可以參考python的doc:Extending Python with C or C++

1. 配置VS環境

A. 此處選擇Release和x64,Debug親測會有問題,win32沒有做過測試。
在這裏插入圖片描述
B. 添加項目的包含目錄。
在這裏插入圖片描述
C. 添加項目的庫目錄
在這裏插入圖片描述
D. 添加附加包含目錄
在這裏插入圖片描述
E. 添加附加庫目錄
在這裏插入圖片描述

F. 修改VS中文件的配置類型,修改爲動態庫
在這裏插入圖片描述

2. C++示例代碼

module文件名爲Py2Cpp.cpp

2.1. 有輸入輸出參數

2.1.1. 常規變量類型

第4節的圖中有python變量類型與c++變量類型的對應關係,對於共有的基礎類型而言,無需手動進行變量的轉換就可以在python中對c++函數進行調用。

/*
導入python API,
由於Python可能會定義一些會影響某些系統上標準標頭的預處理器定義,
因此必須在包含任何標準標頭文件之前包含Python.h。
*/
#include <Python.h> 

/*
cpp中的原始函數
*/
int Add(int x, int y)
{
	return x + y;
}

/*
Python調用Add函數時的接口。
該函數輸入參數爲python變量類型,
PyArg_ParseTuple檢查輸入的參數類型並轉換爲C變量類型,
最後Py_BuildValue又將C變量類型轉換爲python類型。
*/
static PyObject *
WrappAdd(PyObject* self, PyObject* args)
{
	int x, y;
	if (!PyArg_ParseTuple(args, "ii", &x, &y))
	{
		return NULL;
	}
	return Py_BuildValue("i", Add(x, y));
}

/*
爲使得函數能夠被python調用,需要先定義一個方發表“method table”。 
第三個參數(METH_VARARGS),這個標誌指定會使用C的調用慣例。
可選值有METH_VARARGS、METH_VARARGS|METH_KEYWORDS。值0代表使用PyArg_ParseTuple()的過時的變量。
如果單獨使用METH_VARARGS,函數會等待Python傳來tuple格式的參數,並最終使用PyArg_ParseTuple()進行解析。
METH_KEYWORDS值表示接受關鍵字參數。這種情況下C函數需要接受第三個PyObject*對象,表示字典參數,
使用 PyArg_ParseTupleAndKeywords()來解析出參數。
*/
static PyMethodDef test_methods[] = {
	{ "Add", WrappAdd, METH_VARARGS, "something" },
	{ NULL, NULL , 0, NULL }
};

/*
上一個方法表必須被模塊定義結構所引用。
這個結構體必須傳遞給解釋器的模塊初始化函數。
*/
static struct PyModuleDef PCmodule = {
	PyModuleDef_HEAD_INIT,
	"Py2Cpp",   /* name of module */
	NULL, /* module documentation, may be NULL */
	-1,       /* size of per-interpreter state of the module,
			  or -1 if the module keeps state in global variables. */
	test_methods
};

/*
初始化函數必須命名爲PyInit_name(),其中name是module的名字,並應該定義爲非static,且在模塊文件裏。
*/
PyMODINIT_FUNC
PyInit_Py2Cpp(void)
{
	return PyModule_Create(&PCmodule);
}

2.1.2. 非常規變量類型

若c++函數的輸入是vector等類型,則在傳入python變量之後需要將其轉換爲c++的vector類型,才能正常地調用函數。
注意,python中在調用該c++函數時,傳入的變量類型應爲list(可以是多維),最後獲取的輸出變量也是list,可以自行再將其轉換爲numpy類型。

#include <Python.h> 
#include <vector>
#include <iostream>
using namespace std;

/*
將c++中的vector<double>轉換爲python的list
*/
PyObject *
c2p_oneDData(vector<double> *c_data)
{
	int labelSize = c_data->size();
	printf("c++ data size:%d\n", labelSize);
	PyObject *py_data = PyList_New(labelSize);

	for (int i = 0; i < labelSize; ++i) {
		PyList_SetItem(py_data, i, Py_BuildValue("d", (*c_data)[i]));
	}
	return py_data;
}

/*
將c++中的vector<vector<double>>轉換爲python的二維list
*/
PyObject *
c2p_twoDData(vector<vector<double>> *c_data)
{
	int argSize = c_data->size();
	printf("c++ data size:%d\n", argSize);
	PyObject *py_data = PyList_New(argSize);

	for (int i = 0; i < c_data->size(); ++i) {
		PyObject *pObj = PyList_New((*c_data)[i].size());
		for (int j = 0; j < (*c_data)[i].size(); ++j) {
			PyList_SetItem(pObj, j, Py_BuildValue("d", (*c_data)[i][j]));
		}
		PyList_SetItem(py_data, i, Py_BuildValue("O", pObj));
	}
	return py_data;
}

/*
將二維python數據類型轉換爲c++中的vector<vector<double>>類型
*/
vector<vector<double>> *
p2C_twoDData(PyObject *py_data)
{
	vector<vector<double>> *c_data = new vector<vector<double>>;
	int retSize = PyList_Size(py_data);
	printf("python data size:%d\n", retSize);
	for (int i = 0; i < retSize; ++i) {
		PyObject *pItem = PyList_GetItem(py_data, i);
		int itemLen = PyList_Size(pItem);
		vector<double> tmp;
		for (int j = 0; j < itemLen; ++j) {
			PyObject *pItem_j = PyList_GetItem(pItem, j);
			double item_val = PyFloat_AsDouble(pItem_j);
			tmp.push_back(item_val);
		}
		c_data->push_back(tmp);
	}
	return c_data;
}

/*
將一維python數據類型轉換爲c++中的vector<vector<double>>類型
*/
vector<double> *
p2C_oneDData(PyObject *py_data)
{
	vector<double> *c_data = new vector<double>;
	int retSize = PyList_Size(py_data);
	printf("python data size:%d\n", retSize);
	for (int i = 0; i < retSize; ++i) {
		PyObject *pItem = PyList_GetItem(py_data, i);
		double item_val = PyFloat_AsDouble(pItem);
		c_data->push_back(item_val);
	}
	return c_data;
}

vector<vector<double>> 
Add(vector<double> a) // python中需調用的c++函數主體
{
	vector<vector<double>> sum;
	for (int i = 0; i < a.size(); i++) {
		sum.push_back(a);
	}
	return sum;
}

static PyObject *
WrappAdd(PyObject* self, PyObject* args)
{
	PyObject *py_data; // python中傳入的輸入參數
	if (!PyArg_ParseTuple(args, "O", &py_data)) // 獲取到python文件中的輸入參數(python 2 c++)
	{
		return NULL;
	}
	vector<double> a = *p2C_oneDData(py_data); // python 2 c++
	vector<vector<double>> sum = Add(a);
	PyObject * tmp = c2p_twoDData(&sum); // c++ 2 python
	return Py_BuildValue("O", tmp);  // (c++ 2 python)
}

static PyMethodDef test_methods[] = {
	{ "Add", WrappAdd, METH_VARARGS, "something" },
	{ NULL, NULL , 0, NULL }
};

static struct PyModuleDef PCmodule = {
	PyModuleDef_HEAD_INIT,
	"Py2Cpp",   /* name of module */
	NULL, /* module documentation, may be NULL */
	-1,       /* size of per-interpreter state of the module,
			  or -1 if the module keeps state in global variables. */
	test_methods
};

PyMODINIT_FUNC
PyInit_Py2Cpp(void)
{
	return PyModule_Create(&PCmodule);
}

2.2. 無輸入輸出參數

#include <Python.h> 
#include <iostream>

void Add()
{
	std::cout<<"~~"<<std::endl;
}

/*
如果C函數沒有返回值(返回 void 的函數),則必須返回None(可以用Py_RETUN_NONE宏來完成):
*/
static PyObject *
WrappAdd(PyObject* self, PyObject* args)
{
	Add();
	Py_INCREF(Py_None);
	return Py_None;
}

static PyMethodDef test_methods[] = {
	{ "Add", WrappAdd, METH_VARARGS, "something" },
	{ NULL, NULL , 0, NULL }
};

static struct PyModuleDef PCmodule = {
	PyModuleDef_HEAD_INIT,
	"Py2Cpp",   /* name of module */
	NULL, /* module documentation, may be NULL */
	-1,       /* size of per-interpreter state of the module,
			  or -1 if the module keeps state in global variables. */
	test_methods
};

PyMODINIT_FUNC
PyInit_Py2Cpp(void)
{
	return PyModule_Create(&PCmodule);
}

爲每個模塊增加一個型如PyMethodDef ModuleMethods[]的數組,以便於Python解釋器能夠導入並調用它們,每一個數組都包含了函數在Python中的名字,相應的包裝函數的名字以及一個METH_VARARGS常量,METH_VARARGS表示參數以tuple形式傳入。 若需要使用PyArg_ParseTupleAndKeywords()函數來分析命名參數的話,還需要讓這個標誌常量與METH_KEYWORDS常量進行邏輯與運算常量 。數組最後用兩個NULL來表示函數信息列表的結束[1]。^{[1]}

所有工作的最後一部分就是模塊的初始化函數,調用Py_InitModule()函數,並把模塊名和ModuleMethods[]數組的名字傳遞進去,以便於解釋器能正確的調用模塊中的函數[1]。^{[1]}

3. 生成.pyd文件

A. 右鍵點擊項目選擇“生成”:
在這裏插入圖片描述
B. 找到生成的.dll文件,將其後綴更改爲.pyd
在這裏插入圖片描述
C. 將該.pyd文件移動到.py目錄中,可以在.py中直接import該文件
在這裏插入圖片描述
在這裏插入圖片描述

4. 變量類型關係

從Python到C的轉換用PyArg_Parse*系列函數,int PyArg_ParseTuple():把Python傳過來的參數轉爲C;int PyArg_ParseTupleAndKeywords()與PyArg_ParseTuple()作用相同,但是同時解析關鍵字參數;它們的用法跟C的sscanf函數很像,都接受一個字符串流,並根據一個指定的格式字符串進行解析,把結果放入到相應的指針所指的變量中去,它們的返回值爲1表示解析成功,返回值爲0表示失敗。從C到Python的轉換函數是PyObject* Py_BuildValue():把C的數據轉爲Python的一個對象或一組對象,然後返回之;Py_BuildValue的用法跟sprintf很像,把所有的參數按格式字符串所指定的格式轉換成一個Python的對象[1]。^{[1]}

C與Python之間數據轉換的轉換代碼[1]:^{[1]}
在這裏插入圖片描述

5. 注意事項

  • .cpp文件運行時需要別的.dll,則也應將這些.dll.pyd一同複製到.py文件目錄中。否則會報ImportError: DLL load failed: 找不到指定的模塊

Conferences

[1] Python與C/C++的混合調用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章