Python3快速入門(十七)——Python擴展模塊開發

Python3快速入門(十七)——Python擴展模塊開發

一、Python擴展模塊

1、Python擴展模塊簡介

Python與C/C++交互的方案有多種,如Python C API,SWIG,SIP,ctypes,cpython,cffi,boost.python等。
Python只是一個語言規範,有很多具體實現,CPython是標準Python,由C編寫,Python腳本被編譯成CPython字節碼,然後由虛擬機解釋執行,垃圾回收使用引用計數,Python與C/C++混合編程本質是基於CPython解釋器。其它的Python實現包括Jython、IronPython、PyPy、Pyston,Jython是Java編寫的,使用JVM的垃圾回收,可以與Java混合編程,IronPython面向.NET平臺。
Python擴展模塊可以用Python編寫,也可以用C/C++編譯型的語言來寫擴展。Python可擴展性具有爲語言增加新功能、具有可定製性、代碼可以實現複用等優點。

2、Python擴展模塊的特點

(1)提升計算能力
Python擴展模塊使用C/C++編寫,其計算性能也是C/C++同級別的,其跨語言通信接口上的性能損失小到忽略不計,所以能夠提供非常好的性能支持,典型如用於科學計算的Numpy包,其底層調用了第三方的數學計算庫,其性能也是同級別的。
(2)使用多核心計算能力
Python擴展模塊通過對GIL的控制,可以使用CPU的多核心計算能力,而不會受限於純Python程序的單核心限制,結合多線程可以定製使用多個核心。
(3)系統組件隔離和模塊化
通過把每個C/C++函數提供給Python接口,使得函數之間不共享任何狀態,實現了良好的組件隔離,有助於開發和測試。同時由於參數全部通過Python傳遞,易於打印和中斷,可調試性有很大的提高。
(4)使用第三方庫
對於不支持Python的第三方庫,需要開發者自己編寫擴展模塊實現系統對接。但現代流行的大型庫,很多都有官方的Python擴展模塊,使得應用質量有了較大提高,典型如OpenCV和PyCUDA。

二、Python C API擴展

1、Python C API擴展簡介

CPython是C語言實現的Python解釋器,是Python語言的官方實現,是使用最廣泛的Python解釋器。
C/C++實現Python擴展模塊的流程如下:
(1)包含頭文件Python.h
(2)C/C++模塊實現
(3)定義C/C++函數的Python接口映射表
(4)初始化函數
(5)初始化模塊
(6)setup.py編寫
(7)擴展模塊編譯安裝

2、Python頭文件

Python.h頭文件包含用於將C/C++模塊hook到CPython解析器的CPython API,而且必須將Python.h頭文件寫在任何標準頭文件前,因爲Python.h頭文件可能定義了一些影響標準頭文件的預處理宏。
Python.h文件定義了所有的Python C API,Python C API的方法與變量前綴爲Py_和Py,在代碼中儘量不要使用此前綴,避免混亂。
Python.h文件中,Python對象API命名爲`PyObject
、內存管理函數API命名爲PyMen_、數值(包括整數和浮點數的運算等)API命名爲PyNumber*、浮點數API命名爲PyFloat、整數API命名爲PyLong_、序列API命令爲PySequence*、列表API命名爲PyList、元組API命名爲PyTuple_、字典API命名爲PyDict*、集合API命名爲PySet、可迭代對象API命名爲PyIter_、字符串API命名爲PyUnicode*、函數參數API命名爲PyArg、函數API命名爲PyFunction_、文件對象API命名爲PyFile_*`。
C語言沒有bool類型,Python C API的bool類型定義在asdl.h中,形式如下:typedef enum {false, true} bool;,即false=0,true=1。

3、C/C++模塊編寫

實現一個加法和乘法操作的模塊,加法和乘法操作方法如下:

static double add(double a, double b)
{
    return a + b;
}

static double mul(double a, double b)
{
    return a * b;
}

4、C/C++模塊包裝

Python C 擴展的函數定義一般如下:

static PyObject *MyFunction( PyObject *self, PyObject *args );
static PyObject *MyFunctionWithKeywords(PyObject *self,  PyObject *args, PyObject *kw);
static PyObject *MyFunctionWithNoArgs( PyObject *self );

Python C擴展模塊中的函數是靜態函數,名字是任意的,但通常命名爲modulename_functionname的形式,返回PyObject類型的指針。如果函數不想返回一個值,Python定義了一個宏Py_RETURN_NONE,等價於在腳本層返回None。
C/C++函數的包裝如下:

static PyObject* operator_add(PyObject *self, PyObject *args)
{
    float a, b;
    if(!PyArg_ParseTuple(args, "ff", &a, &b))
    {
        return NULL;
    }
    float result = add(a, b);
    return Py_BuildValue("f", result);
}

static PyObject* operator_mul(PyObject *self, PyObject *args)
{
    float a, b;
    if(!PyArg_ParseTuple(args, "ff", &a, &b))
    {
        return NULL;
    }
    float result = mul(a, b);
    return Py_BuildValue("f", result);
}

5、Python接口映射表定義

Python接口映射表是PyMethodDef結構的數組,PyMethodDef結構體定義如下:

struct PyMethodDef {
   char *ml_name;
   PyCFunction ml_meth;
   int ml_flags;
   char *ml_doc;
};

ml_name:暴露給Python程序的函數名。
ml_meth: 函數的指針,即函數定義的地方。
ml_flags: 函數簽名方式,一般是METH_VARARGS;如果想傳入關鍵字參數,可以與MET_KEYWORDS進行或運算;如果不接受任何參數,可以給其賦值爲METH_NOARGS。
ml_doc: 函數的文檔字符串,可以直接給其賦值爲NULL。
Python接口映射表必須以一個由NULL和0組成的結構體進行結尾,示例如下:

static PyMethodDef operator_methods[] = {
   { "add", (PyCFunction)operator_add, METH_VARARGS, "operator add" },
   { "mul", (PyCFunction)operator_mul, METH_VARARGS, "operator mul" },
   { NULL, NULL, 0, NULL }
};

6、擴展模塊初始化

擴展模塊的初始化函數會在模塊被導入時被CPython解析器調用。初始化函數需要從構建的庫中導出,因此Python頭文件裏定義了PyMODINIT_FUNC來進行導出工作,因此需要在定義初始化函數時使用。

PyMODINIT_FUNC initModuleName() {
   Py_InitModule3(ModuleName, module_methods, "docstring...");
}

py_InitModule3函數原型如下:
PyObject* Py_InitModule3(char *name, PyMethodDef *methods, char *doc)
module_name: 被導出的模塊名;
module_methods: 模塊的方法映射表;
docstring: 模塊的註釋;
返回值:返回一個新的模塊對象
Py_InitModule函數原型如下:
PyObject* Py_InitModule(char *name, PyMethodDef *methods)
name:模塊名稱
methods:模塊函數描述表
返回值:返回一個新的模塊對象
operator模塊的初始化如下:

PyMODINIT_FUNC initoperator()
{
   Py_InitModule3("operator", operator_methods);
}
operator.c文件如下:
#include <Python.h>

/***************************
* C++語言函數定義
***************************/

static double add(double a, double b)
{
    return a + b;
}

static double mul(double a, double b)
{
    return a * b;
}

/*****************************
* C++語言函數的包裝
*****************************/

static PyObject* operator_add(PyObject *self, PyObject *args)
{
    float a, b;
    if(!PyArg_ParseTuple(args, "ff", &a, &b))
    {
        return NULL;
    }
    float result = add(a, b);
    return Py_BuildValue("f", result);
}

static PyObject* operator_mul(PyObject *self, PyObject *args)
{
    float a, b;
    if(!PyArg_ParseTuple(args, "ff", &a, &b))
    {
        return NULL;
    }
    float result = mul(a, b);
    return Py_BuildValue("f", result);
}

static PyMethodDef operator_methods[] = {
   { "add", (PyCFunction)operator_add, METH_VARARGS, "operator add" },
   { "mul", (PyCFunction)operator_mul, METH_VARARGS, "operator mul" },
   { NULL, NULL, 0, NULL }
};

PyMODINIT_FUNC initoperator()
{
   Py_InitModule3("operator", operator_methods);
}

7、setup.py編寫

setup.py腳本是用於構建擴展模塊的配置,通常包含如下實用功能:
(1)提供了更完善的編譯參數,比如編譯時特定的宏定義,編譯器參數等
(2)引用其它第三方庫
(3)區分不同平臺的編譯,對Linux和Mac有所區分
(4)提供更加完善的元信息
setup.py腳本如下:

# !/usr/bin/env python

from distutils.core import setup, Extension

setup(name='operator', ext_modules=[Extension('operator', sources=['operator.c'])])

8、Python擴展模塊編譯安裝

編譯模塊:
python setup.py build
安裝模塊:
python setup.py install
Python擴展模塊將會安裝在當前虛擬環境下。
python setup.py build_ext --inplace
--inplace表示在源碼處生成模塊文件

9、擴展模塊使用

import operator

if __name__ == '__main__':
    result = operator.add(1, 9)
    print(result)
    result = operator.mul(100, 1)
    print(result)

# output:
# 10
# 100

10、GIL與多線程

GIL是限制Python使用多核的直接原因,根本原因是Python解釋器內部有一些全局變量,如異常處理等,但由於有很多Python API和第三方模塊在使用GIL全局變量,使得GIL的改進一直得不到進展。
在Python擴展模塊層面是可以釋放GIL的,使得CPU控制權交還給Python,而當前C/C++代碼也可以繼續執行。但任何Python API的調用都必須在GIL控制下進行,因此在執行密集計算的任務前釋放GIL,完成計算後,重新申請GIL,再進行返回值和異常處理。
第一種方法如下:

static PyObject *fun(PyObject *self, PyObject *args) 
{
    //....    
    PyThreadState *_save;    
    _save=PyEval_SaveThread();    
    block();    
    PyEval_RestoreThread(_save);
    //... }
}

第二種方法如下:

Py_BEGIN_ALLOW_THREADS; 
//可能阻塞的操作 
Py_END_ALLOW_THREADS;

使用多核計算的方法是,把任務拆分成多個小份,每個小份都放在一個線程中運行。線程裏調用擴展模塊中的計算函數,計算函數在實際計算時釋放GIL。

11、異常處理

當一個函數失敗時,CPython解釋器約定會返回一個錯誤值(NULL)並設置3個全局靜態變量,分別對應Python的sys.exec_type, sys.exec_value和sys.exec_traceback。最先檢測到異常的函數應該報告並設置全局變量,其它調用函數只是返回異常值。
Python C API定義了一些函數來設置並檢查各種異常:
(1)PyErr_SetString(PyObject* type, const char* message)
type是一個預定義的對象,例如PyExc_ZeroDivisionError,C字符串用於說明異常出現的原因。
(2)PyErr_SetObject(PyObject* type, PyObject* value)
type異常類型,value爲異常值
(3)PyErr_Occurred()
用來檢查是否設置了一個異常
(4)如果要忽略一個異常而不傳遞給解析器,可以調用PyErr_Clear()函數
(5)所有直接調用malloc()或者realloc()函數失敗時,必須要調用PyErr_NoMemory(),並且返回失敗標誌
常見異常類型如下:
PyExc_ZeroDivisionError :被0除    
PyExc_IOError :IO錯誤    
PyExc_TypeError :類型錯誤,如參數類型不對    
PyExc_ValueError :值的範圍錯誤    
PyExc_RuntimeError :運行時錯誤    
PyExc_OSError :各種與OS交互時的錯誤

三、Python C API類型轉化

1、Python對象構建

Py_BuildValue函數可以用於創建PyObject對象,其函數聲明如下:
Python3快速入門(十七)——Python擴展模塊開發

2、基礎數據類型

Python提供了一系列的函數用於C++與Python數據類型的相互轉化,相應函數的格式爲PyXXX_AsXXX 或者PyXXX_FromXXX,一般帶有As的函數是將Python對象轉化爲C++數據類型的,而帶有From的函數是將C++對象轉化爲Python,Py的XXX表示的是Python中的數據類型。PyUnicode_AsWideCharString 將Python中的字符串轉化爲C++中寬字符,而 PyUnicode_FromWideChar 是將C++的字符串轉化爲Python中的字符串。Python3廢除了Python2中的普通的字符串,將所有字符串都當做Unicode,所以使用Python3時需要將所有字符串轉化爲Unicode。

#include <Python.h>
#include <stdio.h>

int main() {

    // 初始化Python環境
    Py_Initialize();

    // 有符號整型
    PyObject* py_ival1 = Py_BuildValue("i", -890);
    PyObject* py_ival2 = PyLong_FromLong(-890);
    int ival1 = PyLong_AsLong(py_ival1);
    int ival2 = PyLong_AsLong(py_ival2);
    printf("ival1 = %d, ival2 = %d\n", ival1, ival2);
    // ival1 = -890, ival2 = -890

    // 無符號整型
    PyObject* py_uval1 = Py_BuildValue("I", 123456789);
    PyObject* py_uval2 = PyLong_FromUnsignedLong(123456789);
    unsigned int uval1 = PyLong_AsUnsignedLong(py_uval1);
    unsigned int uval2 = PyLong_AsUnsignedLong(py_uval2);
    printf("uval1 = %d, uval2 = %d\n", uval1, uval2);
    // uval1 = 123456789, uval2 = 123456789

    // 長整型
    PyObject* py_lval1 = Py_BuildValue("L", 456784567845678);
    PyObject* py_lval2 = PyLong_FromLongLong(456784567845678);
    long long lval1 = PyLong_AsLongLong(py_lval1);
    long long lval2 = PyLong_AsLongLong(py_lval2);
    printf("lval1 = %lld, lval2 = %lld\n", lval1, lval2);
    // lval1 = 456784567845678, lval2 = 456784567845678

    // 浮點類型
    PyObject* py_fval1 = Py_BuildValue("f", 3.1415);
    PyObject* py_fval2 = PyFloat_FromDouble(3.1415);
    double fval1 = PyFloat_AsDouble(py_fval1);
    double fval2 = PyFloat_AsDouble(py_fval2);
    printf("fval1 = %f, fval2 = %f\n", fval1, fval2);
    // fval1 = 3.141500, fval2 = 3.141500

    // 布爾類型
    PyObject* py_bval1 = Py_BuildValue("b", false);
    PyObject* py_bval2 = PyBool_FromLong(true);
    int bval1 = PyLong_AsLong(py_bval1);
    int bval2 = PyLong_AsLong(py_bval2);
    printf("bval1 = %d, bval2 = %d\n", bval1, bval2);
    // bval1 = 0, bval2 = 1

    // 字符串類型
    PyObject* py_sval1 = Py_BuildValue("s", "hello world");
    PyObject* py_sval2 = PyUnicode_FromString("hello world");
    // 將unicode轉換爲utf8
    PyObject* py_utf1 = PyUnicode_AsUTF8String(py_sval1);
    PyObject* py_utf2 = PyUnicode_AsUTF8String(py_sval2);
    // 將utf8轉換爲const char*
    char* sval1 = PyBytes_AsString(py_utf1);
    char* sval2 = PyBytes_AsString(py_utf2);
    printf("sval1 = %s, sval2 = %s\n", sval1, sval2);
    // sval1 = hello world, sval2 = hello world

    // 退出Python環境
    Py_Finalize();
    return 0;
}

G++編譯:
g++ -I/home/user/anaconda3/include/python3.7m -c pycobject.c
鏈接:
g++ -o main pycobject.o -L/usr/local/lib -lpython3.7 -lrt -lpthread -lutil -ldl

3、Python元組對象

PyObject* PyTuple_New(Py_ssize_t len)
創建一個Python元組對象,必須設置長度,如果設置長度爲0,則元組對象是一個空元組。
int PyTuple_Check(PyObject *p)
判斷是否是一個元組對象
Py_ssize_t PyTuple_Size(PyObject *p)
獲取元組的大小
PyObject* PyTuple_GetItem(PyObject* p, Py_ssize_t pos)
獲取元組內指定下標的值
PyObject* PyTuple_GetSlice(PyObject* p, Py_ssize_t low, Py_ssize_t high)
獲取分片數據 p[lwo, higt]
int PyTuple_SetItem(PyObject* p, Py_ssize_t pos, PyObject* o)
設置元組指定下標的值
int _PyTuple_Resize(PyObject *p, Py_ssize_t newsize)
改變元組的大小

#include <Python.h>
#include <stdio.h>

int main() {

    // 初始化Python環境
    Py_Initialize();

    PyObject* tuple = PyTuple_New(3);
    PyObject* item0 = Py_BuildValue("i", 123);
    PyTuple_SetItem(tuple, 0, item0);

    PyObject* item1 = Py_BuildValue("s", "hello");
    PyTuple_SetItem(tuple, 1, item1);

    PyObject* item2 = Py_BuildValue("f", 23.98f);
    PyTuple_SetItem(tuple, 2, item2);

    PyObject* py_data = PyTuple_GetItem(tuple, 0);
    int value = PyLong_AsLong(py_data);
    printf("value = %d\n", value);
    // value = 123

    // 退出Python環境
    Py_Finalize();
    return 0;
}

4、Python字典對象

PyObject* PyDict_New()
創建一個Python字典對象,成功返回新空字典,失敗返回NULL。
int PyDict_Check(PyObject *p)
判斷對象是不是一個字典
void PyDict_Clear(PyObject *p)
清空Python字典對象的數據
int PyDict_Contains(PyObject* p, PyObject* key)
判斷字典內是否存在一個鍵值數據
PyObject* PyDict_Copy(PyObject* p)
拷貝一個字典的數據,產生一個新的Python字典對象
int PyDict_SetItem(PyObject* p, PyObject* key, PyObject *val)
給Python字典對象設置新的鍵值數據
int PyDict_SetItemString(PyObject* p, const char key, PyObject *val)
給Python字典對象設置新的鍵值數據
int PyDict_DelItem(PyObject* p, PyObject* key)
刪除Python鍵值數據
int PyDict_DelItemString(PyObject* p, const char key)
刪除Python鍵值數據
PyObject* PyDict_GetItem(PyObject* p, PyObject *key)
獲取Python字典對象的鍵的值
PyObject* PyDict_GetItemString(PyObject* p, const char *key)
獲取Python字典對象的鍵的值
PyObject* PyDict_SetDefault(PyObject* p, PyObject* key, PyObject* default)
設置Python字典對象的默認值,當獲取的Key不存在的時候則返回當前的默認數據
PyObject* PyDict_Items(PyObject* p)
返回一個Python字典對象所有數據的PyListObject
PyObject* PyDict_Keys(PyObject* p)
返回一個Python字典對象的所有Key
PyObject* PyDict_Values(PyObject* p)
返回一個Python字典對象的所有Value數據
Py_ssize_t PyDict_Size(PyObject *p)
獲取Python字典的大小 len(dict)
int PyDict_Next(PyObject* p, Py_ssize_t ppos, PyObject* pkey, PyObject* pvalue)
遍歷獲取Python字典對象的所有數據,

#include <Python.h>
#include <stdio.h>

int main() {

    // 初始化Python環境
    Py_Initialize();

    PyObject* dict = PyDict_New();
    PyObject* key1 = Py_BuildValue("s", "age");
    PyObject* value1 = Py_BuildValue("i", 30);
    PyDict_SetItem(dict, key1, value1);

    PyObject* py_data = PyDict_GetItemString(dict, "age");
    int value = PyLong_AsLong(py_data);
    printf("value = %d\n", value);
    // value = 30

    // 退出Python環境
    Py_Finalize();
    return 0;
}

5、Python列表對象

PyObject* PyList_New(Py_ssize_t len)
創建一個列表,成功返回新列表,失敗返回NULL
int PyList_Check(PyObject *p)
判斷是否是一個Python List(列表)
Py_ssize_t PyList_Size(PyObject *list)
獲取列表元素的個數 len(list)
PyObject* PyList_GetItem(PyObject* list, Py_ssize_t index)
從列表裏面獲取一個元素,計數器不會加1
int PyList_SetItem(PyObject* list, Py_ssize_t index, PyObject* item)
設置別表指定位置的值,下標的所在的位置必須是有值的,並且是有效的
int PyList_Insert(PyObject* list, Py_ssize_t index, PyObject* item)
在列表指定位置插入值
int PyList_Append(PyObject* list, PyObject* item)
在列表尾部追加值
PyObject* PyList_GetSlice(PyObject* list, Py_ssize_t low, Py_ssize_t high)
獲取列表裏面一段切片數據,一段指定範圍的數據 list[low:higt]
int PyList_SetSlice(PyObject* list, Py_ssize_t low, Py_ssize_t high, PyObject* itemlist)
設置列表分片數據,指定列表範圍的數據 list[low:higt] = itemlist
int PyList_Sort(PyObject *list)
對列表數據進行排序
int PyList_Reverse(PyObject *list)
把列表裏面的所有數據反轉
PyObject* PyList_AsTuple(PyObject* list)
將Python列表轉爲Python元組 tuple(list)

#include <Python.h>
#include <stdio.h>

int main() {

    // 初始化Python環境
    Py_Initialize();

    PyObject* list = PyList_New(5);
    PyObject* item0 = Py_BuildValue("i", 123);
    PyList_SetItem(list, 0, item0);

    PyObject* item1 = Py_BuildValue("s", "hello");
    PyList_SetItem(list, 1, item1);

    PyObject* item2 = Py_BuildValue("f", 23.98f);
    PyList_SetItem(list, 2, item2);

    PyObject* py_data = PyList_GetItem(list, 0);
    int value = PyLong_AsLong(py_data);
    printf("value = %d\n", value);
    // value = 123

    // 退出Python環境
    Py_Finalize();
    return 0;
}

6、參數提取

Python腳本調用擴展模塊的函數時,傳入的參數會存在PyObject* args所指向的PyObject對象中,參數的提取使用PyArg_ParseTuple() 和 PyArg_ParseTupleAndKeywords()函數進行解析 。
int PyArg_ParseTuple(PyObject* arg, char* format, ...);
參數 arg 是一個tuple對象,包含Python傳遞來的參數, format 參數必須是格式化字符串。
int PyArg_ParseTupleAndKeywords(PyObject* arg, PyObject* kwdict, char* format, char* kwlist[],...);
參數 arg 是一個tuple對象,包含Python傳遞過來的參數,參數 kwdict 是關鍵字字典,用於接受運行時傳來的關鍵字參數。參數 kwlist 是一個NULL結尾的字符串,定義了可以接受的參數名,並從左到右與format中各個變量對應。如果執行成功 PyArg_ParseTupleAndKeywords() 會返回true,否則返回false並拋出異常。
format參數是一個字符串,通常每個字符代表一種類型;剩下的參數是與format相對應的各個變量的地址,返回值是一個整型,解析成功返回1,解析出錯返回0。
無參函數的參數提取:
ok = PyArg_ParseTuple(args, "");
參數爲一個字符串的函數的參數提取:
ok = PyArg_ParseTuple(args, "s", &s);
參數爲兩個長整型與一個字符串的函數的參數提取:
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s);
參數至少有一個字符串,可以另外有一個字符串或整型的函數的參數提取:
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
參數爲兩個元組的函數的參數提取:
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",&left, &top, &right, &bottom, &h, &v);
參數爲一個PyObject對象,可以表示Python中的任意類型。
ok = PyArg_ParseTuple(args, "O", &p);

四、C++調用Python腳本

1、Python C API常用接口

void Py_Initialize()
初始化Python解釋器,在C++程序中使用其它Python C API前,必須初始化Python解釋器,如果調用失敗,將產生一個致命的錯誤。
int PyRun_SimpleString( const char *command)
執行一段Python代碼。
PyObject* PyImport_ImportModule( char *name)
導入一個Python模塊,參數name可以是.py文件的文件名。
`PyObject
PyModule_GetDict( PyObject module)<br/>獲取模塊名稱空間下的字典對象<br/>PyObject PyRun_String( const char str, int start, PyObject globals, PyObject locals)<br/>執行一段Python代碼。<br/>int PyArg_Parse( PyObject args, char format, ...)<br/>解析Python數據爲C的類型<br/>PyObject PyObject_GetAttrString( PyObject o, char attr_name)<br/>返回模塊對象o中的attr_name 屬性或函數<br/>PyObject Py_BuildValue( char format, ...)<br/>構建一個參數列表,把C類型轉換爲Python對象,使Python可以使用C類型數據<br/>PyEval_CallObject(PyObject pfunc, PyObject pargs)<br/>pfunc是要調用的Python 函數,通常可用PyObject_GetAttrString()獲得;pargs是函數的參數列表,通常可用Py_BuildValue()構建<br/>void Py_Finalize()`
關閉Python解釋器,釋放解釋器所佔用的資源。

2、Python環境初始化

調用Python模塊時需要首先包含Python.h頭文件,Python.h頭文件一般在安裝Python目錄中的 include文件中。
調用Python腳本前先調用Py_Initialize 函數來初始化Python環境,可以調用Py_IsInitialized來檢測Python環境是否初始化成功。

3、調用Python語句

針對簡單的Python語句,可以直接調用 PyRun_SimpleString 函數來執行, 接收參數爲Python語句的ANSI字符串,返回int型的值。如果爲0表示執行成功,否則爲失敗。

4、調用Python函數

(1)加載Python模塊(自定義模塊)
加載Python模塊需要調用 PyImport_ImportModule 函數,傳入模塊名稱作爲參數,模塊名稱即py文件名稱,不能帶.py後綴。返回一個Python對象的指針,在C++中表示爲PyObject,即模塊對象的指針。
(2)獲取Python函數對象
調用 PyObject_GetAttrString 函數來加載對應的Python模塊中的方法,接收兩個參數,第一個參數是獲取到的對應模塊的指針,第二個參數是函數名稱的ANSI字符串。返回一個對應Python函數的對象指針。
(3)檢查Python函數對象可調用性
調用 PyCallable_Check可以檢測Python函數對象是否可以被調用,接收參數爲Python函數對象指針,如果能被調用會返回true否則返回false。
(4)參數傳遞
Python中函數的參數以元組的方式傳入,需要先將要傳入的參數轉化爲元組。
(5)Python函數調用
調用 PyObject_CallObject 函數來執行對應的Python函數,接收兩個參數,第一個參數Python函數對象的指針,第二個參數是需要傳入Python函數中的參數組成的元組。返回Python的元組對象。
(6)解析返回值
獲取到返回值(Python元組對象)後使用對應的函數將Python元組轉化爲C++中的變量。

5、釋放資源

需要調用 Py_DECREF 來解除Python對象的引用,以便Python的垃圾回收器能正常的回收Python對象的內存。

6、退出Python環境

Py_Finalize();

7、C++調用Python腳本實例

util.py腳本如下:

def add(a, b):
    return a + b

def mul(a, b):
    return a * b

def power(a, b):
    return a ** b

C++調用Python代碼如下:

#include <Python.h>
#include <stdio.h>

int main(int argc, char** argv)
{
    // 初始化Python
    Py_Initialize();

    // 檢查初始化是否成功
    if (!Py_IsInitialized())
    {
        return -1;
    }
    // 添加當前路徑
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('./')");

    // 載入Python模塊
    PyObject* pName = PyUnicode_FromString("util");
    PyObject* pModule = PyImport_Import(pName);
    if (!pModule)
    {
        printf("can't find util.py\n");
        return -1;
    }
    PyObject* pDict = PyModule_GetDict(pModule);
    if ( !pDict )
    {
        return -1;
    }
    PyObject* result;
    int a = 0;
    int b = 0;
    PyObject* pFunc = PyDict_GetItemString(pDict, "add");
    if ( !pFunc || !PyCallable_Check(pFunc) )
    {
        printf("can't find function add\n");
        return -1;
    }

    // 參數進棧
    PyObject* pArgs;
    pArgs = PyTuple_New(2);
    a = 3;
    b = 4;
    PyTuple_SetItem(pArgs, 0, Py_BuildValue("l",a));
    PyTuple_SetItem(pArgs, 1, Py_BuildValue("l",b));

    // 調用Python函數
    result = PyObject_CallObject(pFunc, pArgs);
    printf("%d add %d == %d\n", a, b, PyLong_AsLong(result));

    pFunc = PyDict_GetItemString(pDict, "mul");
    if ( !pFunc || !PyCallable_Check(pFunc) )
    {
        printf("can't find function mul\n");
        return -1;
    }

    pArgs = PyTuple_New(2);
    a = 2;
    b = 3;
    PyTuple_SetItem(pArgs, 0, Py_BuildValue("l",a));
    PyTuple_SetItem(pArgs, 1, Py_BuildValue("l",b));

    result = PyObject_CallObject(pFunc, pArgs);
    printf("%d mul %d == %d\n", a, b, PyLong_AsLong(result));

    pFunc = PyDict_GetItemString(pDict, "power");
    if ( !pFunc || !PyCallable_Check(pFunc) ) {
        printf("can't find function power\n");
        getchar();
        return -1;
     }
    pArgs = PyTuple_New(2);
    a = 2;
    b = 3;
    PyTuple_SetItem(pArgs, 0, Py_BuildValue("l",a));
    PyTuple_SetItem(pArgs, 1, Py_BuildValue("l",b));
    result = PyObject_CallObject(pFunc, pArgs);
    printf("%d power %d == %d\n", a, b, PyLong_AsLong(result));

    Py_DECREF(pName);
    Py_DECREF(pArgs);
    Py_DECREF(pModule);

    // 關閉Python
    Py_Finalize();
    return 0;
}

G++編譯:
g++ -I/home/user/anaconda3/include/python3.7m -c main.cpp
鏈接:
g++ -o main main.o -L/usr/local/lib -lpython3.7m -lrt -lpthread -lutil -ldl

五、ctypes擴展

1、ctypes簡介

ctypes是Python的一個可以鏈接C/C++的庫,可以將C/C++函數編譯成動態鏈接庫,即window下的.dll文件或者是linux下的.so文件,通過使用cytpes可以直接調用動態連接庫的C/C++函數,加速代碼的運行速度。
ctypes的優點如下:
(1)不要修改動態庫的源碼
(2)只需要動態庫和頭文件
(3)使用比較簡單,而且目前大部分庫都兼容C/C++

import platform
from ctypes import *

if __name__ == '__main__':
    if platform.system() == 'Windows':
        libc = cdll.LoadLibrary('msvcrt.dll')
    elif platform.system() == 'Linux':
        libc = cdll.LoadLibrary('libc.so.6')

    libc.printf(bytes('Hello world!\n', 'utf-8'))

 ctypes作爲連接Python和C的接口,,其對應的數據類型如下:
Python3快速入門(十七)——Python擴展模塊開發
Python 中的類型,除了 None,int, long, Byte String,Unicode String 作爲 C 函數的參數默認提供轉換外,其它類型都必須顯式提供轉換。None:對應 C 中的 NULL,int、long對應 C 中的 int,具體實現時會根據機器字長自動適配。
在python3中,Byte String對應 C 中的一個字符串指針char *,指向一塊內存區域,通常字符串前面需要加小b,  b"helloworld";Unicode String對應 C 中一個寬字符串指針 wchar_t *,指向一塊內存區域,Python3中對應的是字符串,如"helloworld"。

2、Python調用C動態連接庫

編寫C語言函數add.c文件:

#include <stdlib.h>

int add(int a, int b)
{
    return a + b;
}

使用GCC編譯C語言文件:
gcc -o libadd.so -shared -fPIC add.c
如果使用g++編譯生成C動態庫的代碼中的函數時,需要使用extern "C"來進行編譯。
Python調用C語言動態鏈接庫:

import ctypes

if __name__ == '__main__':
    loader = ctypes.cdll.LoadLibrary
    lib = loader("./libadd.so")
    result = lib.add(1, 2)
    print(result)

# output:
# 3

3、Python調用C++動態連接庫

需要extern "C"來輔助,也就是說還是隻能調用C函數,不能直接調用方法,但是能解析C++方法。如果不用extern "C",構建後的動態鏈接庫將沒有函數的符號表。
編寫C++語言函數add.cpp文件:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

using namespace std;

class Utils
{
public:
    static void display(const char* name, int age)
    {
        printf("%s %d\n", name, age);
    }
};

extern "C"{
    void display(const char* name, int age)
    {
        Utils::display(name, age);
    }
}

G++編譯:
g++ -o libadd.so -shared -fPIC add.cpp
Python調用C++語言動態鏈接庫:

import ctypes

if __name__ == '__main__':
    loader = ctypes.cdll.LoadLibrary
    lib = loader("./libadd.so")
    name = bytes("Bauer", 'utf-8')
    lib.display(name, 28)

4、Python調用C/C++可執行程序

main.cpp文件:

#include <stdio.h>

void display()
{
    printf("Hello CPython\n");
}

int main(int argc, const char* argv[])
{
    display();
    return 0;
}

G++編譯:
g++ -o main main.cpp
Python調用可執行程序:

import os

if __name__ == '__main__':
    os.system("./main")

六、SWIG擴展

1、SWIG簡介

SWIG(Simplified Wrapper and Interface Generator)是用來爲腳本語言調用C和C++程序的軟件開發工具,實際上是一個編譯器,獲取C/C++的聲明和定義,用一個殼封裝起來,以便其它腳本語言訪問。SWIG 最大的好處就是將腳本語言的開發效率和 C/C++ 的運行效率有機的結合起來。
SWIG安裝:
pip install swig

2、編寫C/C++模塊

utils.h文件如下:

#include <stdio.h>

class utils
{
public:
    static void display();
};

utils.cpp文件如下:

#include "utils.h"

void utils::display()
{
    printf("Hello SWIG\n");
}

3、編寫規則轉換接口文件

swig封裝需要一個.i後綴文件的封裝說明。
%module &lt;name&gt;爲封裝名稱,Python調用包名是&lt;name&gt;
%{...%}內是附加的函數說明和頭文件,源文件以外的部分都要包括在內,包括頭文件和宏定義等。
utils.i文件如下:

%module utils
%{
/* Includes the header in the wrapper code */
#include "utils.h"
%}
/* Parse the header file to generate wrappers */
%include "utils.h"

4、生成封裝文件

C++文件命令如下:
swig -python -c++ utils.i
C文件命令如下:
swig -python utils.i
swig會生成兩個不同的文件:utils_wrap.cxx(c源碼是utils_wrap.c)和python文件utils.py。

5、編譯生成模塊

編寫setup.py文件:

from distutils.core import setup, Extension

utils_module = Extension('_utils', sources=['utils.cpp', 'utils_wrap.cxx'])
setup(name='utils', version='0.1', author="bauer", description="""Simple swig C++/Python example""",
      ext_modules=[utils_module], py_modules=["utils"])

可以使用include_dirs指定搜索的頭文件路徑,librarydirs指定搜索的庫路徑。
swig生成的擴展模塊對象名必須使用python模塊名並在前面加上下劃線`
,通過swig生成的python文件是utils.py,模塊對象名必須是’_utils’,否則無法順利編譯。<br/>在當前目錄下編譯生成Python模塊:<br/>python setup.py build_ext --inplace<br/>當前目錄下生成模塊如下:<br/>_utils.cpython-37m-x86_64-linux-gnu.so`

6、Python擴展模塊使用

import utils

if __name__ == '__main__':
    utils.utils_display()

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