自定义 Python 第一部分:扩展






3.82/5 (15投票s)
如何为 Python 构建 C/C++ 自定义扩展库。
引言
这是 Python 系列文章的第一部分,共两部分。本文将介绍使用 C/C++ 扩展 Python 的基本技术。第二篇文章将介绍将 Python 解释器嵌入到 C/C++ 应用程序中的技术。虽然我很少直接使用 Python 扩展,但这里介绍的信息对于嵌入式使用是绝对必需的。
本文并非 Python 入门教程。它假定您已掌握 Python 语言。
您需要安装 Python 发行版才能构建和使用示例。Python 主要有两个发行商:Python.org 和 ActiveState.com。两者都可以,但我现在更偏爱 ActiveState 的版本。他们将帮助文件编译成 Windows HTML 帮助文件,我觉得比基本发行版更容易导航。而且它还附带了所有的 Windows 扩展库。
两者都附带 include 和 libs 目录,所以您无需下载源代码。然而,在本文的第二部分将需要源代码。
扩展 - 它们是什么?
Python 的座右铭是“自带电池”。这意味着 Python 在开箱即用的情况下功能非常强大。它附带了许多额外的模块,为用户提供了访问套接字、CGI、URL 解析和 HTTP 支持、XML 处理、MIME、线程,甚至 XML-based RPC 等功能的权限。尽管有所有这些额外功能,但我们总是需要/想要进行自定义。
扩展模块有两种形式:原生 Python 和 C/C++ 动态链接库 (DLL)。原生 Python 模块就是可以被用户脚本导入的 Python 脚本。创建原生 Python 模块就像编写 Python 脚本一样简单。
C/C++ 扩展模块是经过编译的 DLL,它们有一个标准的导出函数,用于处理模块初始化和注册。本文将介绍这些基于 DLL 的扩展模块。
API
Python 是用 C 编写的,其作者们很乐意公开和记录大部分解释器内部结构。正是通过这个 API,我们才获得了扩展语言所需的访问权限。
Python 对象
在 Python 中,每个对象,甚至每个值,在内部都表示为 PyObject
。PyObject
是一个结构,它定义了所有的处理程序入口点,并维护类型信息和引用计数。Python 扩展编程的基础之一是,无论何时您在 C/C++ 中操作任何 Python 对象,您都将操作一个 PyObject
。
话虽如此,您很少会使用 PyObject
API 例程。相反,您将使用适用于正在操作的具体 Python 类型的 API 例程。具体细节请参阅 Python C/C++ API 文档。
引用计数
Python 使用引用计数机制来处理基本的内存管理。每个对象都有一个引用计数,当对象被复制时,计数会增加,当对象引用被释放时,计数会减少。
class doodle: # etc. d = doodle() # reference count = 1 e = d # reference count = 2 del(d) # reference count = 1 del(e) # reference count = 0 object deleted
在 C/C++ 中操作 Python 对象时,必须注意引用计数。某些函数(在 Python C API 文档中标记)会返回新对象;其他函数会返回借用的引用。使用 Py_INCREF()
和 Py_DECREF()
宏来辅助您。另外,我建议您在每个可疑的 API 调用旁边添加注释,说明其返回类型。
PyObject *pList = PyList_New(5); // new reference ... PyObject *pItem = PyList_GetItem(2); // borrowed reference ... Py_DECREF(pList);
引用计数很重要!我花了很多时间来追踪内存泄漏,结果发现是我在应该 Py_DECREF()
对象时却没有这样做。
Python 类型
Python 中有六种主要的本地数据类型:整数、浮点数、字符串、元组、列表和字典。Python 支持多种其他本地类型(复数、长整数等),其使用将留给读者自行练习。
整数、浮点数和字符串
这些与您的预期一样。您只需要知道如何构建和操作它们。
// build an integer PyObject *pInt = Py_BuildValue("i", 147); // new reference assert(PyInt_Check(pInt)); int i = PyInt_AsLong(pInt); Py_DECREF(pInt); // build a float PyObject *pFloat = Py_BuildValue("f", 3.14159f); // new reference assert(PyFloat_Check(pFloat)); float f = PyFloat_AsDouble(pFloat); Py_DECREF(pFloat); // build a string PyObject *pString = Py_BuildValue("s", "yabbadabbadoo"); // new reference assert(PyString_Check(pString); int nLen = PyString_Size(pString); char *s = PyString_AsString(pString); Py_DECREF(pString);
元组
元组是固定长度的不可变数组。当 Python 脚本调用 C/C++ 扩展方法时,所有非关键字参数都将作为元组传递。不用说,解析这个元组通常是您在方法中所做的第一件事。
这是一些元组使用的混合示例
// create the tuple PyObject *pTuple = PyTuple_New(3); // new reference assert(PyTuple_Check(pTuple)); assert(PyTuple_Size(pTuple) == 3); // set the item PyTuple_SetItem(pTuple, 0, Py_BuildValue("i", 1)); PyTuple_SetItem(pTuple, 1, Py_BuildValue("f", 2.0f)); PyTuple_SetItem(pTuple, 2, Py_BuildValue("s", "three")); // parse tuple items int i; float f; char *s; if(!PyArg_ParseTuple(pTuple, "ifs", &i, &f, &s)) PyErr_SetString(PyExc_TypeError, "invalid parameter"); // cleanup Py_DECREF(pTuple);
PyArg_ParseTuple()
可能是最常用的 API 函数之一。第二个参数是一个字符串,用于指定元组中预期的对象类型。ifs
表示:整数、浮点数、字符串。有关详细解释和其他类型字符的列表,请参阅 API 文档。
列表
列表类似于 STL 向量。它们允许随机访问和遍历存储的对象。这是一个典型的列表使用示例
// create the list PyObject *pList = PyList_New(5); // new reference assert(PyList_Check(pList)); // set some initial values for(int i = 0; i < 5; ++i) PyList_SetItem(pList, i, Py_BuildValue("i", i)); // insert an item PyList_Insert(pList, 3, Py_BuildValue("s", "inserted")); // append an item PyList_Append(pList, Py_BuildValue("s", "appended")); // sort the list PyList_Sort(pList); // reverse the list PyList_Reverse(pList); // fetch and manipulate a list slice PyObject *pSlice = PyList_GetSlice(pList, 2, 4); // new reference for(int j = 0; j < PyList_Size(pSlice); ++j) { PyObject *pValue = PyList_GetItem(pList, j); assert(pValue); } Py_DECREF(pSlice); // cleanup Py_DECREF(pList);
字典
字典相当于 STL 映射。它们将键映射到值。这是一个典型的字典使用示例
// create the dictionary PyObject *pDict = PyDict_New(); // new reference assert(PyDict_Check(pDict)); // add a few named values PyDict_SetItemString(pDict, "first", Py_BuildValue("i", 1)); PyDict_SetItemString(pDict, "second", Py_BuildValue("f", 2.0f)); // enumerate all named values PyObject *pKeys = PyDict_Keys(); // new reference for(int i = 0; i < PyList_Size(pKeys); ++i) { PyObject *pKey = PyList_GetItem(pKeys, i); // borrowed reference PyObject *pValue = PyDict_GetItem(pDict, pKey); // borrowed reference assert(pValue); } Py_DECREF(pKeys); // remove a named value PyDict_DelItemString(pDict, "second"); // cleanup Py_DECREF(pDict);
扩展概念
扩展模块通常由三部分组成:实际的导出函数、方法表和初始化函数。
首先,我们将看一个简单的扩展模块示例。然后我们将逐个检查每个部分。
一个典型的扩展模块看起来是这样的
// exported yinkle() function static PyObject *wanklib_yinkle(PyObject *pSelf, PyObject *pArgs) { char *szString; int nInt; float fFloat; PyObject *pList; if(!PyArg_ParseTuple(pArgs, "sifo", &szString, &nInt, &fFloat, &pList)) { PyErr_SetString(PyExc_TypeError, "yinkle() invalid parameter"); return NULL; } if(!PyList_Check(pList)) { PyErr_SetString(PyExc_TypeError, "yinkle() fourth parameter must be a list"); return NULL; } PyList_Append(pList, Py_BuildObject("f", strlen(szString) * nInt / fFloat)); Py_INCREF(Py_None); return Py_None; } // wanklib method table static PyMethodDef WankLibMethods[] = { {"yinkle", wanklib_yinkle, METH_VARARGS, "Do a bit of stuff."}, {NULL, NULL, 0, NULL} }; // wanklib initialization function void initwanklib(void) { // initialize module PyObject *pModule = Py_InitModule("wanklib", WankLibMethods); // fetch module dictionary and add non-function symbols PyObject *pDict = PyModule_GetDict(pModule); PyDict_SetItemString(pDict, "eleven", Py_BuildValue("i", 147)); PyDict_SetItemString(pDict, "doubleyew", Py_BuildValue("s", "kay")); }
初始化函数
每个扩展模块都必须导出一个名为 initmodule
的函数。当 Python 脚本请求导入该模块时,Python 会查询该库以查找该确切命名的函数并调用它。初始化函数负责告知 Python 有关它所导出的函数、变量和类的信息。
方法表
初始化函数将调用 Python 例程 Py_InitModule()
来注册模块方法。它将传递新模块将被知晓的名称,以及描述导出方法的表。每个表条目由四部分组成:可调用名称字符串、函数本身、描述参数如何传递的参数,以及一个文档字符串。表中的最后一个条目需要是一个带有 NULL
条目的哨兵。
名称字符串是该方法可以从 Python 中调用的名称。参数类型标记可以是 METH_VARARGS
或 METH_KEYWORDS
。METH_VARARGS
是传递参数的标准方式;参数将打包在元组中。指定 METH_KEYWORDS
会请求将命名参数传递给字典。
方法
所有扩展方法都具有相同的原型(假设它们被标记为 METH_VARARGS
)
PyObject *method(PyObject *pSelf, PyObject *pArgs);
所有扩展方法都必须返回一个 PyObject
指针。如果函数没有实际的返回值,您必须在增加其引用后返回一个指向全局“None”对象的指针。
PyObject *method(PyObject *pSelf, PyObject *pArgs) {
Py_INCREF(Py_None);
return Py_None;
}
要表示发生了错误并抛出 Python 异常,您必须返回 NULL
并设置错误字符串
PyObject *method(PyObject *pSelf, PyObject *pArgs) { PyErr_SetString(PyExc_StandardError, "something bad happened"); return NULL; }
扩展方法的第一个参数是“self”指针,并且在构建自定义类时才真正有效。这些将在下一篇文章中详细介绍。
第二个参数是一个元组,其中包含按顺序排列的每个参数。如上所述,解析此元组通常是发生的第一件事。
变量
每个 Python 模块都有一个本地对象字典。为了从您的模块导出变量,您所要做的就是将它们添加到此字典中。Py_InitModule()
返回一个指向已初始化模块的指针。PyModule_GetDict()
检索本地对象字典。
PyObject *pModule = Py_InitModule("wanklib", wankLibMethods); PyObject *pDict = PyModule_GetDict(pModule); PyDict_SetItemString(pDict, "someVar", Py_BuildValue("i", 147));
实现
在 Windows 中,Python 扩展只是具有已知导出符号(初始化函数)的 DLL 文件。为了构建扩展,您必须在 Visual Studio 中创建一个 Win32 动态链接库项目。选择“导出一个或多个符号的 DLL”,这样您就有了一个可以工作的模板。我相信您可以使用 MFC AppWizard 来构建扩展,但我从未尝试过,也不打算尝试。
简单的扩展可以构建在单个文件中,并将遵循上面示例中所示的布局。
所有 Python API 声明都通过包含一个文件来访问:Python.h。它位于 Python 安装目录下的 include 子目录中。与其硬编码路径,不如将目录添加到您的 Tools/Options/Directories 列表中。在您进行此操作时,也请将 libs 子目录添加到库文件搜索路径列表中。
无需显式链接 Python 库。Python.h include 文件使用 pragma 指令强制进行正确的链接。
注意:Python.h 中的 pragma 指令强制链接 Python 的调试版本:Python22_d.lib(版本可能因您安装的版本而异)。如果您尚未下载 Python 源代码,您可能没有这个库。您的选择是下载并构建调试版本,或者在发布模式下构建您的扩展。
为了移除 C++ 名称修饰,您需要将您的初始化函数定义为 extern "C"
。
最后,编译完成后,将您的 DLL 文件放置在 Python 安装目录下的 DLLs 子目录中。当您尝试导入它时,它将自动被拾取。
除此之外,您应该已经准备就绪。
示例
我包含的示例只是封装了 Mersenne Twister 伪随机数生成器。我使用了“发明者” Makoto Matsumoto 和 Takuji Nishimura 的原始代码。mtprng
模块提供了两个方法:sgenrand()
用于播种生成器,genrand()
用于生成 [0,1] 范围内的数字。该代码使用 VS6 和 SP5,并安装了 Python 2.2 进行编译,但任何 1.5.3 以上版本的 Python 应该都可以。
祝您好运!
历史
- 2002 年 11 月 21 日 - 创建。