65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义 Python 第一部分:扩展

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.82/5 (15投票s)

2002 年 11 月 22 日

CC (ASA 2.5)

8分钟阅读

viewsIcon

86897

downloadIcon

960

如何为 Python 构建 C/C++ 自定义扩展库。

引言

这是 Python 系列文章的第一部分,共两部分。本文将介绍使用 C/C++ 扩展 Python 的基本技术。第二篇文章将介绍将 Python 解释器嵌入到 C/C++ 应用程序中的技术。虽然我很少直接使用 Python 扩展,但这里介绍的信息对于嵌入式使用是绝对必需的。

本文并非 Python 入门教程。它假定您已掌握 Python 语言。

您需要安装 Python 发行版才能构建和使用示例。Python 主要有两个发行商:Python.orgActiveState.com。两者都可以,但我现在更偏爱 ActiveState 的版本。他们将帮助文件编译成 Windows HTML 帮助文件,我觉得比基本发行版更容易导航。而且它还附带了所有的 Windows 扩展库。

两者都附带 includelibs 目录,所以您无需下载源代码。然而,在本文的第二部分将需要源代码。

扩展 - 它们是什么?

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 中,每个对象,甚至每个值,在内部都表示为 PyObjectPyObject 是一个结构,它定义了所有的处理程序入口点,并维护类型信息和引用计数。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_VARARGSMETH_KEYWORDSMETH_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 日 - 创建。
© . All rights reserved.