新聞中心
使用 C 擴展為 Python 提供特定功能。

大渡口網(wǎng)站建設公司創(chuàng)新互聯(lián),大渡口網(wǎng)站設計制作,有大型網(wǎng)站制作公司豐富經(jīng)驗。已為大渡口上千余家提供企業(yè)網(wǎng)站建設服務。企業(yè)網(wǎng)站搭建\成都外貿網(wǎng)站建設公司要多少錢,請找那個售后服務好的大渡口做網(wǎng)站的公司定做!
在前一篇文章中,我介紹了 ??六個 Python 解釋器??。在大多數(shù)系統(tǒng)上,CPython 是默認的解釋器,而且根據(jù)民意調查顯示,它還是最流行的解釋器。Cpython 的獨有功能是使用擴展 API 用 C 語言編寫 Python 模塊。用 C 語言編寫 Python 模塊允許你將計算密集型代碼轉移到 C,同時保留 Python 的易用性。
在本文中,我將向你展示如何編寫一個 C++ 擴展模塊。使用 C++ 而不是 C,因為大多數(shù)編譯器通常都能理解這兩種語言。我必須提前說明缺點:以這種方式構建的 Python 模塊不能移植到其他解釋器中。它們只與 CPython 解釋器配合工作。因此,如果你正在尋找一種可移植性更好的與 C 語言模塊交互的方式,考慮下使用 ??ctypes?? 模塊。
源代碼
和往常一樣,你可以在 ??GitHub?? 上找到相關的源代碼。倉庫中的 C++ 文件有以下用途:
- ?
?my_py_module.cpp??: Python 模塊??MyModule?? 的定義 - ?
?my_cpp_class.h??: 一個頭文件 - 只有一個暴露給 Python 的 C++ 類 - ?
?my_class_py_type.h/cpp??: Python 形式的 C++ 類 - ?
?pydbg.cpp??: 用于調試的單獨應用程序
本文構建的 Python 模塊不會有任何實際用途,但它是一個很好的示例。
構建模塊
在查看源代碼之前,你可以檢查它是否能在你的系統(tǒng)上編譯。??我使用 CMake?? 來創(chuàng)建構建的配置信息,因此你的系統(tǒng)上必須安裝 CMake。為了配置和構建這個模塊,可以讓 Python 去執(zhí)行這個過程:
$ python3 setup.py build
或者手動執(zhí)行:
$ cmake -B build$ cmake --build build
之后,在 ??/build?? 子目錄下你會有一個名為 ??MyModule. so?? 的文件。
定義擴展模塊
首先,看一下 ??my_py_module.cpp?? 文件,尤其是 ??PyInit_MyModule?? 函數(shù):
PyMODINIT_FUNCPyInit_MyModule(void) { PyObject* module = PyModule_Create(&my_module); PyObject *myclass = PyType_FromSpec(&spec_myclass); if (myclass == NULL){ return NULL; } Py_INCREF(myclass); if(PyModule_AddObject(module, "MyClass", myclass) < 0){ Py_DECREF(myclass); Py_DECREF(module); return NULL; } return module;}這是本例中最重要的代碼,因為它是 CPython 的入口點。一般來說,當一個 Python C 擴展被編譯并作為共享對象二進制文件提供時,CPython 會在同名二進制文件中(???)搜索 ??PyInit_? 函數(shù),并在試圖導入時執(zhí)行它。
無論是聲明還是實例,所有 Python 類型都是 ??PyObject?? 的一個指針。在此函數(shù)的第一部分中,??module?? 通過 ??PyModule_Create(...)?? 創(chuàng)建的。正如你在 ??module?? 詳述(??my_py_module??,同名文件)中看到的,它沒有任何特殊的功能。
之后,調用 ??PyType_FromSpec?? 為自定義類型 ??MyClass?? 創(chuàng)建一個 Python ??堆類型?? 定義。一個堆類型對應于一個 Python 類,然后將它賦值給 ??MyModule?? 模塊。
注意,如果其中一個函數(shù)返回失敗,則必須減少以前創(chuàng)建的復制對象的引用計數(shù),以便解釋器刪除它們。
指定 Python 類型
??MyClass?? 詳述在 ??my_class_py_type.h?? 中可以找到,它作為 ??PyType_Spec?? 的一個實例:
static PyType_Spec spec_myclass = { "MyClass", // name sizeof(MyClassObject) + sizeof(MyClass), // basicsize 0, // itemsize Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // flags MyClass_slots // slots};它定義了一些基本類型信息,它的大小包括 Python 表示的大?。??MyClassObject??)和普通 C++ 類的大小(??MyClass??)。??MyClassObject?? 定義如下:
typedef struct { PyObject_HEAD int m_value; MyClass* m_myclass;} MyClassObject;Python 表示的話就是 ??PyObject?? 類型,由 ??PyObject_HEAD?? 宏和其他一些成員定義。成員 ??m_value?? 視為普通類成員,而成員 ??m_myclass?? 只能在 C++ 代碼內部訪問。
??PyType_Slot?? 定義了一些其他功能:
static PyType_Slot MyClass_slots[] = { {Py_tp_new, (void*)MyClass_new}, {Py_tp_init, (void*)MyClass_init}, {Py_tp_dealloc, (void*)MyClass_Dealloc}, {Py_tp_members, MyClass_members}, {Py_tp_methods, MyClass_methods}, {0, 0} /* Sentinel */};在這里,設置了一些初始化和析構函數(shù)的跳轉,還有普通的類方法和成員,還可以設置其他功能,如分配初始屬性字典,但這是可選的。這些定義通常以一個哨兵結束,包含 ??NULL?? 值。
要完成類型詳述,還包括下面的方法和成員表:
static PyMethodDef MyClass_methods[] = { {"addOne", (PyCFunction)MyClass_addOne, METH_NOARGS, PyDoc_STR("Return an incrmented integer")}, {NULL, NULL} /* Sentinel */};static struct PyMemberDef MyClass_members[] = { {"value", T_INT, offsetof(MyClassObject, m_value)}, {NULL} /* Sentinel */};在方法表中,定義了 Python 方法 ??addOne??,它指向相關的 C++ 函數(shù) ??MyClass_addOne??。它充當了一個包裝器,它在 C++ 類中調用 ??addOne()?? 方法。
在成員表中,只有一個為演示目的而定義的成員。不幸的是,在 ??PyMemberDef?? 中使用的 ??offsetof?? 不允許添加 C++ 類型到 ??MyClassObject??。如果你試圖放置一些 C++ 類型的容器(如 ??std::optional??),編譯器會抱怨一些內存布局相關的警告。
初始化和析構
??MyClass_new?? 方法只為 ??MyClassObject?? 提供一些初始值,并為其類型分配內存:
PyObject *MyClass_new(PyTypeObject *type, PyObject *args, PyObject *kwds){ std::cout << "MtClass_new() called!" << std::endl; MyClassObject *self; self = (MyClassObject*) type->tp_alloc(type, 0); if(self != NULL){ // -> 分配成功 // 賦初始值 self->m_value = 0; self->m_myclass = NULL; } return (PyObject*) self;}實際的初始化發(fā)生在 ??MyClass_init?? 中,它對應于 Python 中的 ??__init__()?? 方法:
int MyClass_init(PyObject *self, PyObject *args, PyObject *kwds){ ((MyClassObject *)self)->m_value = 123; MyClassObject* m = (MyClassObject*)self; m->m_myclass = (MyClass*)PyObject_Malloc(sizeof(MyClass)); if(!m->m_myclass){ PyErr_SetString(PyExc_RuntimeError, "Memory allocation failed"); return -1; } try { new (m->m_myclass) MyClass(); } catch (const std::exception& ex) { PyObject_Free(m->m_myclass); m->m_myclass = NULL; m->m_value = 0; PyErr_SetString(PyExc_RuntimeError, ex.what()); return -1; } catch(...) { PyObject_Free(m->m_myclass); m->m_myclass = NULL; m->m_value = 0; PyErr_SetString(PyExc_RuntimeError, "Initialization failed"); return -1; } return 0;}如果你想在初始化過程中傳遞參數(shù),必須在此時調用 ??PyArg_ParseTuple??。簡單起見,本例將忽略初始化過程中傳遞的所有參數(shù)。在函數(shù)的第一部分中,??PyObject?? 指針(??self??)被強轉為 ??MyClassObject?? 類型的指針,以便訪問其他成員。此外,還分配了 C++ 類的內存,并執(zhí)行了構造函數(shù)。
注意,為了防止內存泄漏,必須仔細執(zhí)行異常處理和內存分配(還有釋放)。當引用計數(shù)將為零時,??MyClass_dealloc?? 函數(shù)負責釋放所有相關的堆內存。在文檔中有一個章節(jié)專門講述關于 C 和 C++ 擴展的內存管理。
包裝方法
從 Python 類中調用相關的 C++ 類方法很簡單:
PyObject* MyClass_addOne(PyObject *self, PyObject *args){ assert(self); MyClassObject* _self = reinterpret_cast(self); unsigned long val = _self->m_myclass->addOne(); return PyLong_FromUnsignedLong(val);} 同樣,??PyObject?? 參數(shù)(??self??)被強轉為 ??MyClassObject?? 類型以便訪問 ??m_myclass??,它指向 C++ 對應類實例的指針。有了這些信息,調用 ??addOne()?? 類方法,并且結果以 ??Python 整數(shù)對象?? 返回。
3 種方法調試
出于調試目的,在調試配置中編譯 CPython 解釋器是很有價值的。詳細描述參閱 ??官方文檔??。只要下載了預安裝的解釋器的其他調試符號,就可以按照下面的步驟進行操作。
GNU 調試器
當然,老式的 ??GNU 調試器(GDB)?? 也可以派上用場。源碼中包含了一個 ??gdbinit?? 文件,定義了一些選項和斷點,另外還有一個 ??gdb.sh?? 腳本,它會創(chuàng)建一個調試構建并啟動一個 GDB 會話:
Gnu 調試器(GDB)對于 Python C 和 C++ 擴展非常有用
GDB 使用腳本文件 ??main.py?? 調用 CPython 解釋器,它允許你輕松定義你想要使用 Python 擴展模塊執(zhí)行的所有操作。
C++ 應用
另一種方法是將 CPython 解釋器嵌入到一個單獨的 C++ 應用程序中??梢栽趥}庫的 ??pydbg.cpp?? 文件中找到:
int main(int argc, char *argv[], char *envp[]){ Py_SetProgramName(L"DbgPythonCppExtension"); Py_Initialize(); PyObject *pmodule = PyImport_ImportModule("MyModule"); if (!pmodule) { PyErr_Print(); std::cerr << "Failed to import module MyModule" << std::endl; return -1; } PyObject *myClassType = PyObject_GetAttrString(pmodule, "MyClass"); if (!myClassType) { std::cerr << "Unable to get type MyClass from MyModule" << std::endl; return -1; } PyObject *myClassInstance = PyObject_CallObject(myClassType, NULL); if (!myClassInstance) { std::cerr << "Instantioation of MyClass failed" << std::endl; return -1; } Py_DecRef(myClassInstance); // invoke deallocation return 0;}使用 ??高級接口??,可以導入擴展模塊并對其執(zhí)行操作。它允許你在本地 IDE 環(huán)境中進行調試,還能讓你更好地控制傳遞或來自擴展模塊的變量。
缺點是創(chuàng)建一個額外的應用程序的成本很高。
VSCode 和 VSCodium LLDB 擴展
使用像 ??CodeLLDB?? 這樣的調試器擴展可能是最方便的調試選項。倉庫包含了一些 VSCode/VSCodium 的配置文件,用于構建擴展,如 ??task.json??、??CMake Tools?? 和調用調試器(??launch.json??)。這種方法結合了前面幾種方法的優(yōu)點:在圖形 IDE 中調試,在 Python 腳本文件中定義操作,甚至在解釋器提示符中動態(tài)定義操作。
VSCodium 有一個集成的調試器。
用 C++ 擴展 Python
Python 的所有功能也可以從 C 或 C++ 擴展中獲得。雖然用 Python 寫代碼通常認為是一件容易的事情,但用 C 或 C++ 擴展 Python 代碼是一件痛苦的事情。另一方面,雖然原生 Python 代碼比 C++ 慢,但 C 或 C++ 擴展可以將計算密集型任務提升到原生機器碼的速度。
你還必須考慮 ABI 的使用。穩(wěn)定的 ABI 提供了一種方法來保持舊版本 CPython 的向后兼容性,如 ??文檔?? 所述。
最后,你必須自己權衡利弊。如果你決定使用 C 語言來擴展 Python 中的一些功能,你已經(jīng)看到了如何實現(xiàn)它。
分享題目:為Python寫一個C++擴展模塊
網(wǎng)頁地址:http://fisionsoft.com.cn/article/ccoichg.html


咨詢
建站咨詢
