1. 使用 C 或 C++ 扩展 Python

如果您知道如何用 C 编程,那么为 Python 添加新的内置模块非常容易。这种 扩展模块 可以做 Python 本身无法直接完成的两件事:它们可以实现新的内置对象类型,并且可以调用 C 库函数和系统调用。

为了支持扩展,Python API(应用程序编程接口)定义了一组函数、宏和变量,这些函数、宏和变量提供了对 Python 运行时系统的大多数方面的访问。通过包含头文件 "Python.h",Python API 被整合到 C 源文件中。

扩展模块的编译取决于其预期用途以及您的系统设置;详细信息将在后面的章节中给出。

注意

C 扩展接口是特定于 CPython 的,扩展模块在其他 Python 实现中不起作用。在许多情况下,可以避免编写 C 扩展并保持对其他实现的移植性。例如,如果您的用例是调用 C 库函数或系统调用,您应该考虑使用 ctypes 模块或 cffi 库,而不是编写自定义 C 代码。这些模块允许您编写 Python 代码来与 C 代码交互,并且比编写和编译 C 扩展模块在 Python 实现之间具有更好的移植性。

1.1. 一个简单的例子

让我们创建一个名为 spam 的扩展模块(蒙提·派森粉丝最喜欢的食物……),假设我们想为 C 库函数 system() [1] 创建一个 Python 接口。此函数以一个以 null 结尾的字符字符串作为参数,并返回一个整数。我们希望此函数可以从 Python 中以以下方式调用

>>> import spam
>>> status = spam.system("ls -l")

首先创建一个名为 spammodule.c 的文件。(从历史上看,如果一个模块名为 spam,包含其实现的 C 文件名为 spammodule.c;如果模块名称很长,例如 spammify,模块名称可以简化为 spammify.c。)

我们文件的前两行可以是

#define PY_SSIZE_T_CLEAN
#include <Python.h>

它会引入 Python API(您可以添加一个注释来描述模块的用途和版权声明,如果您愿意)。

注意

由于 Python 可能会定义一些影响某些系统上标准头文件的预处理器定义,因此您必须在包含任何标准头文件之前包含 Python.h

建议在包含 Python.h 之前始终定义 PY_SSIZE_T_CLEAN。有关此宏的描述,请参见 在扩展函数中提取参数

所有由 Python.h 定义的用户可见符号都以 PyPY 为前缀,除了那些在标准头文件中定义的符号。为了方便起见,并且由于它们被 Python 解释器广泛使用,"Python.h" 包含一些标准头文件:<stdio.h><string.h><errno.h><stdlib.h>。如果您的系统上不存在最后一个头文件,它将直接声明函数 malloc()free()realloc()

我们添加到模块文件中的下一件事是 C 函数,它将在评估 Python 表达式 spam.system(string) 时被调用(我们很快就会看到它是如何被调用的)。

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    return PyLong_FromLong(sts);
}

从 Python 中的参数列表(例如,单个表达式 "ls -l")到传递给 C 函数的参数,有一个直接的转换。C 函数始终有两个参数,习惯上分别命名为 selfargs

self 参数指向模块级函数的模块对象;对于方法,它将指向对象实例。

args 参数将是一个指向包含参数的 Python 元组对象的指针。元组的每个项目对应于调用参数列表中的一个参数。参数是 Python 对象——为了在我们的 C 函数中对它们进行任何操作,我们必须将它们转换为 C 值。Python API 中的函数 PyArg_ParseTuple() 检查参数类型并将它们转换为 C 值。它使用模板字符串来确定所需的参数类型以及要将转换后的值存储其中的 C 变量的类型。稍后将详细介绍。

PyArg_ParseTuple() 如果所有参数都具有正确的类型并且其组件已存储在传递其地址的变量中,则返回真(非零)。如果传递了无效的参数列表,则返回假(零)。在后一种情况下,它还会引发适当的异常,以便调用函数可以立即返回 NULL(正如我们在示例中看到的)。

1.2. 插曲:错误和异常

在整个 Python 解释器中,一个重要的约定是:当函数失败时,它应该设置一个异常条件并返回一个错误值(通常为 -1NULL 指针)。异常信息存储在解释器线程状态的三个成员中。如果不存在异常,则它们为 NULL。否则,它们是 Python 元组成员的 C 等效项,该元组由 sys.exc_info() 返回。这些是异常类型、异常实例和回溯对象。了解它们对于理解错误是如何传递的非常重要。

Python API 定义了许多函数来设置各种类型的异常。

最常见的是 PyErr_SetString()。它的参数是异常对象和 C 字符串。异常对象通常是预定义的对象,如 PyExc_ZeroDivisionError。C 字符串指示错误的原因,并被转换为 Python 字符串对象并存储为异常的“关联值”。

另一个有用的函数是 PyErr_SetFromErrno(),它只接受一个异常参数,并通过检查全局变量 errno 来构建关联的值。最通用的函数是 PyErr_SetObject(),它接受两个对象参数,异常及其关联的值。您不需要 Py_INCREF() 传递给这些函数的任何对象。

您可以使用 PyErr_Occurred() 非破坏性地测试是否已设置异常。这将返回当前异常对象,如果未发生异常,则返回 NULL。通常您不需要调用 PyErr_Occurred() 来查看函数调用中是否发生了错误,因为您应该能够从返回值中判断出来。

当一个调用另一个函数 g 的函数 f 检测到后者失败时,f 本身应该返回一个错误值(通常为 NULL-1)。它不应该调用任何 PyErr_* 函数——g 已经调用了一个。然后,f 的调用者应该也向调用者返回一个错误指示,同样调用 PyErr_*,依此类推——错误的最详细原因已经由第一个检测到它的函数报告。一旦错误到达 Python 解释器的主循环,它将中止当前正在执行的 Python 代码,并尝试找到 Python 程序员指定的异常处理程序。

(存在一些情况,模块可以通过调用另一个 PyErr_* 函数来提供更详细的错误消息,在这种情况下,这样做是可以的。但是,作为一般规则,这不是必需的,并且会导致有关错误原因的信息丢失:大多数操作可能由于多种原因而失败。)

要忽略由失败的函数调用设置的异常,必须通过调用 PyErr_Clear() 显式清除异常条件。C 代码唯一应该调用 PyErr_Clear() 的时间是,如果它不想将错误传递给解释器,而是想完全自行处理它(可能通过尝试其他方法,或假装没有错误发生)。

每个失败的 malloc() 调用都必须转换为异常——malloc()(或 realloc())的直接调用者必须调用 PyErr_NoMemory() 并返回一个失败指示器。所有对象创建函数(例如,PyLong_FromLong())已经这样做了,因此此注释仅与直接调用 malloc() 的人相关。

还要注意,除了 PyArg_ParseTuple() 及其朋友之外,返回整数状态的函数通常返回正值或零表示成功,-1 表示失败,就像 Unix 系统调用一样。

最后,在返回错误指示器时,请务必清理垃圾(通过对您已创建的对象进行 Py_XDECREF()Py_DECREF() 调用)!

选择引发哪个异常完全由您决定。存在与所有内置 Python 异常相对应的预声明 C 对象,例如 PyExc_ZeroDivisionError,您可以直接使用它们。当然,您应该明智地选择异常——不要使用 PyExc_TypeError 来表示文件无法打开(这可能应该是 PyExc_OSError)。如果参数列表有问题,PyArg_ParseTuple() 函数通常会引发 PyExc_TypeError。如果您有一个参数,其值必须在特定范围内或必须满足其他条件,则 PyExc_ValueError 是合适的。

您也可以定义一个特定于您的模块的新的异常。为此,您通常在文件开头声明一个静态对象变量

static PyObject *SpamError;

并在您的模块的初始化函数(PyInit_spam())中使用异常对象对其进行初始化

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;

    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;

    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    Py_XINCREF(SpamError);
    if (PyModule_AddObject(m, "error", SpamError) < 0) {
        Py_XDECREF(SpamError);
        Py_CLEAR(SpamError);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

请注意,异常对象的 Python 名称是 spam.errorPyErr_NewException() 函数可能会创建一个类,其基类是 Exception(除非传递了另一个类而不是 NULL),如 内置异常 中所述。

还要注意,SpamError 变量保留对新创建的异常类的引用;这是故意的!由于异常可能会被外部代码从模块中删除,因此需要对该类的拥有引用以确保它不会被丢弃,从而导致 SpamError 成为悬空指针。如果它成为悬空指针,则引发异常的 C 代码可能会导致核心转储或其他意外副作用。

我们将在本示例的后面讨论 PyMODINIT_FUNC 作为函数返回类型的使用。

可以在您的扩展模块中使用对 PyErr_SetString() 的调用来引发 spam.error 异常,如下所示

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    if (sts < 0) {
        PyErr_SetString(SpamError, "System command failed");
        return NULL;
    }
    return PyLong_FromLong(sts);
}

1.3. 回到示例

回到我们的示例函数,您现在应该能够理解此语句

if (!PyArg_ParseTuple(args, "s", &command))
    return NULL;

如果在参数列表中检测到错误,它将返回 NULL(返回对象指针的函数的错误指示器),依赖于由 PyArg_ParseTuple() 设置的异常。否则,参数的字符串值将被复制到局部变量 command 中。这是一个指针赋值,您不应该修改它指向的字符串(因此在标准 C 中,变量 command 应该适当地声明为 const char *command)。

下一条语句是对 Unix 函数 system() 的调用,将我们刚刚从 PyArg_ParseTuple() 获取的字符串传递给它

sts = system(command);

我们的 spam.system() 函数必须将 sts 的值作为 Python 对象返回。这是使用函数 PyLong_FromLong() 完成的。

return PyLong_FromLong(sts);

在这种情况下,它将返回一个整数对象。(是的,即使在 Python 中,整数也是堆上的对象!)

如果您有一个不返回任何有用参数的 C 函数(返回 void 的函数),则相应的 Python 函数必须返回 None。您需要这种习惯用法来做到这一点(它由 Py_RETURN_NONE 宏实现)

Py_INCREF(Py_None);
return Py_None;

Py_None 是特殊 Python 对象 None 的 C 名称。它是一个真正的 Python 对象,而不是一个 NULL 指针,这意味着在大多数情况下“错误”,正如我们所见。

1.4. 模块的方法表和初始化函数

我承诺展示如何从 Python 程序中调用 spam_system()。首先,我们需要在“方法表”中列出它的名称和地址

static PyMethodDef SpamMethods[] = {
    ...
    {"system",  spam_system, METH_VARARGS,
     "Execute a shell command."},
    ...
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

请注意第三个条目(METH_VARARGS)。这是一个标志,告诉解释器要为 C 函数使用的调用约定。它通常应该始终为 METH_VARARGSMETH_VARARGS | METH_KEYWORDS;值为 0 表示使用 PyArg_ParseTuple() 的过时变体。

仅使用 METH_VARARGS 时,函数应期望 Python 级别的参数作为可通过 PyArg_ParseTuple() 解析的元组传递;有关此函数的更多信息,请参见下文。

如果应将关键字参数传递给函数,则可以在第三个字段中设置 METH_KEYWORDS 位。在这种情况下,C 函数应接受第三个 PyObject * 参数,该参数将是关键字的字典。使用 PyArg_ParseTupleAndKeywords() 解析此类函数的参数。

方法表必须在模块定义结构中引用

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

反过来,此结构必须在模块的初始化函数中传递给解释器。初始化函数必须命名为 PyInit_name(),其中 name 是模块的名称,并且应该是模块文件中定义的唯一非 static 项目

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModule_Create(&spammodule);
}

请注意,PyMODINIT_FUNC 将函数声明为 PyObject * 返回类型,声明平台所需的任何特殊链接声明,并且对于 C++,将函数声明为 extern "C"

当 Python 程序首次导入模块 spam 时,将调用 PyInit_spam()。(有关嵌入 Python 的注释,请参见下文。)它调用 PyModule_Create(),该函数返回一个模块对象,并将内置函数对象插入到基于表(PyMethodDef 结构数组)的新创建的模块中,该表位于模块定义中。 PyModule_Create() 返回一个指向它创建的模块对象的指针。它可能会因某些错误而中止并出现致命错误,或者如果模块无法成功初始化,则返回 NULL。init 函数必须将其创建的模块对象返回给调用者,以便它随后被插入到 sys.modules 中。

嵌入 Python 时,除非 PyImport_Inittab 表中存在条目,否则不会自动调用 PyInit_spam() 函数。要将模块添加到初始化表中,请使用 PyImport_AppendInittab(),可以选择在后面导入模块

int
main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }

    /* Add a built-in module, before Py_Initialize */
    if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }

    /* Pass argv[0] to the Python interpreter */
    Py_SetProgramName(program);

    /* Initialize the Python interpreter.  Required.
       If this step fails, it will be a fatal error. */
    Py_Initialize();

    /* Optionally import the module; alternatively,
       import can be deferred until the embedded script
       imports it. */
    PyObject *pmodule = PyImport_ImportModule("spam");
    if (!pmodule) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'spam'\n");
    }

    ...

    PyMem_RawFree(program);
    return 0;
}

注意

sys.modules 中删除条目或将编译后的模块导入进程中的多个解释器(或在 fork() 之后,但没有随后的 exec())可能会为某些扩展模块造成问题。扩展模块作者在初始化内部数据结构时应谨慎。

Python 源代码分发中包含一个更完整的示例模块,名为 Modules/xxmodule.c。此文件可用作模板,或者仅作为示例阅读。

注意

与我们的 spam 示例不同,xxmodule 使用多阶段初始化(Python 3.5 中的新功能),其中 PyModuleDef 结构从 PyInit_spam 返回,模块的创建留给导入机制。有关多阶段初始化的详细信息,请参阅 PEP 489

1.5. 编译和链接

在使用新的扩展之前,还需要做两件事:将其编译并与 Python 系统链接。如果您使用动态加载,详细信息可能取决于系统使用的动态加载方式;有关更多信息,请参阅有关构建扩展模块的章节(第 构建 C 和 C++ 扩展 章)以及仅与 Windows 上的构建相关的附加信息(第 在 Windows 上构建 C 和 C++ 扩展 章)。

如果您无法使用动态加载,或者想要将您的模块作为 Python 解释器的一部分永久存在,则必须更改配置设置并重新构建解释器。幸运的是,这在 Unix 上非常简单:只需将您的文件(例如 spammodule.c)放在解压缩的源代码分发版的 Modules/ 目录中,在文件 Modules/Setup.local 中添加一行来描述您的文件

spam spammodule.o

并在顶层目录中运行 make 来重新构建解释器。您也可以在 Modules/ 子目录中运行 make,但必须首先通过运行“make Makefile”来重新构建 Makefile。(每次更改 Setup 文件时都需要这样做。)

如果您的模块需要链接额外的库,这些库也可以在配置文件中的行中列出,例如

spam spammodule.o -lX11

1.6. 从 C 调用 Python 函数

到目前为止,我们一直专注于使 C 函数可从 Python 调用。反过来也很有用:从 C 调用 Python 函数。对于支持所谓的“回调”函数的库来说,尤其如此。如果 C 接口使用回调,等效的 Python 通常需要向 Python 程序员提供回调机制;实现将需要从 C 回调中调用 Python 回调函数。还可以想象其他用途。

幸运的是,Python 解释器很容易递归调用,并且有一个标准接口来调用 Python 函数。(我不会详细介绍如何使用特定字符串作为输入来调用 Python 解析器——如果您有兴趣,请查看 Python 源代码中 Modules/main.c-c 命令行选项的实现。)

调用 Python 函数很容易。首先,Python 程序必须以某种方式将 Python 函数对象传递给您。您应该提供一个函数(或其他一些接口)来执行此操作。当此函数被调用时,将指向 Python 函数对象的指针保存到全局变量中(注意要 Py_INCREF() 它!)——或者您认为合适的地方。例如,以下函数可能是模块定义的一部分

static PyObject *my_callback = NULL;

static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
    PyObject *result = NULL;
    PyObject *temp;

    if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);         /* Add a reference to new callback */
        Py_XDECREF(my_callback);  /* Dispose of previous callback */
        my_callback = temp;       /* Remember new callback */
        /* Boilerplate to return "None" */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

此函数必须使用 METH_VARARGS 标志注册到解释器;这将在第 模块的方法表和初始化函数 节中介绍。 PyArg_ParseTuple() 函数及其参数在第 提取扩展函数中的参数 节中进行了说明。

Py_XINCREF()Py_XDECREF() 分别增加/减少对象的引用计数,并且在存在 NULL 指针的情况下是安全的(但请注意,在这种情况下,temp 不会是 NULL)。有关它们的更多信息,请参见第 引用计数 节。

稍后,当需要调用该函数时,您可以调用 C 函数 PyObject_CallObject()。该函数有两个参数,都是指向任意 Python 对象的指针:Python 函数和参数列表。参数列表必须始终是一个元组对象,其长度等于参数的数量。要调用没有参数的 Python 函数,请传入 NULL 或一个空元组;要调用带有一个参数的函数,请传入一个单元素元组。 Py_BuildValue() 在其格式字符串由零个或多个括号之间的格式代码组成时返回一个元组。例如

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject() 返回一个 Python 对象指针:这是 Python 函数的返回值。 PyObject_CallObject() 对其参数是“引用计数中立的”。在示例中,创建了一个新的元组作为参数列表,该元组在 PyObject_CallObject() 调用之后立即被 Py_DECREF()

PyObject_CallObject() 的返回值是“新的”:要么是一个全新的对象,要么是一个引用计数已增加的现有对象。因此,除非您想将其保存在全局变量中,否则您应该以某种方式 Py_DECREF() 结果,即使(尤其!)您对它的值不感兴趣。

但是,在此之前,重要的是要检查返回值是否不是 NULL。如果是,则 Python 函数通过引发异常而终止。如果调用 PyObject_CallObject() 的 C 代码是从 Python 调用的,它现在应该向其 Python 调用者返回一个错误指示,以便解释器可以打印堆栈跟踪,或者调用 Python 代码可以处理异常。如果这不可行或不可取,则应通过调用 PyErr_Clear() 来清除异常。例如

if (result == NULL)
    return NULL; /* Pass error back */
...use result...
Py_DECREF(result);

根据对 Python 回调函数的期望接口,您可能还需要向 PyObject_CallObject() 提供参数列表。在某些情况下,参数列表也由 Python 程序通过指定回调函数的相同接口提供。然后可以将其保存并以与函数对象相同的方式使用。在其他情况下,您可能需要构造一个新的元组作为参数列表传递。最简单的方法是调用 Py_BuildValue()。例如,如果您想传递一个整型事件代码,您可以使用以下代码

PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
    return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

请注意 Py_DECREF(arglist) 的放置位置,它紧接在调用之后,在错误检查之前!还要注意,严格来说,此代码并不完整:Py_BuildValue() 可能内存不足,这应该进行检查。

您也可以使用 PyObject_Call() 来调用带关键字参数的函数,它支持参数和关键字参数。如上例所示,我们使用 Py_BuildValue() 来构建字典。

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
    return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

1.7. 在扩展函数中提取参数

PyArg_ParseTuple() 函数声明如下

int PyArg_ParseTuple(PyObject *arg, const char *format, ...);

arg 参数必须是一个元组对象,包含从 Python 传递到 C 函数的参数列表。format 参数必须是一个格式字符串,其语法在 Python/C API 参考手册中的 解析参数和构建值 中解释。其余参数必须是变量的地址,其类型由格式字符串确定。

请注意,虽然 PyArg_ParseTuple() 检查 Python 参数是否具有所需的类型,但它无法检查传递给调用的 C 变量地址的有效性:如果您在那里犯了错误,您的代码可能会崩溃,或者至少会覆盖内存中的随机位。所以要小心!

请注意,提供给调用者的任何 Python 对象引用都是借用引用;不要递减它们的引用计数!

一些示例调用

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* No arguments */
    /* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
    /* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
    /* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* A pair of ints and a string, whose size is also returned */
    /* Possible Python call: f((1, 2), 'three') */
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* A string, and optionally another string and an integer */
    /* Possible Python calls:
       f('spam')
       f('spam', 'w')
       f('spam', 'wb', 100000) */
}
{
    int left, top, right, bottom, h, v;
    ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
             &left, &top, &right, &bottom, &h, &v);
    /* A rectangle and a point */
    /* Possible Python call:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* a complex, also providing a function name for errors */
    /* Possible Python call: myfunction(1+2j) */
}

1.8. 扩展函数的关键字参数

PyArg_ParseTupleAndKeywords() 函数声明如下

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char *kwlist[], ...);

argformat 参数与 PyArg_ParseTuple() 函数的参数相同。kwdict 参数是作为第三个参数从 Python 运行时接收的关键字字典。kwlist 参数是一个以 NULL 结尾的字符串列表,用于标识参数;这些名称与来自 format 的类型信息从左到右匹配。成功时,PyArg_ParseTupleAndKeywords() 返回 true,否则返回 false 并引发适当的异常。

注意

使用关键字参数时,无法解析嵌套元组!传递的关键字参数如果不在 kwlist 中,将导致引发 TypeError

这是一个使用关键字的示例模块,基于 Geoff Philbrick (philbrick@hks.com) 的示例

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";

    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;

    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
           action, voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);

    Py_RETURN_NONE;
}

static PyMethodDef keywdarg_methods[] = {
    /* The cast of the function is necessary since PyCFunction values
     * only take two PyObject* parameters, and keywdarg_parrot() takes
     * three.
     */
    {"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "Print a lovely skit to standard output."},
    {NULL, NULL, 0, NULL}   /* sentinel */
};

static struct PyModuleDef keywdargmodule = {
    PyModuleDef_HEAD_INIT,
    "keywdarg",
    NULL,
    -1,
    keywdarg_methods
};

PyMODINIT_FUNC
PyInit_keywdarg(void)
{
    return PyModule_Create(&keywdargmodule);
}

1.9. 构建任意值

此函数是 PyArg_ParseTuple() 的对应函数。它声明如下

PyObject *Py_BuildValue(const char *format, ...);

它识别一组与 PyArg_ParseTuple() 识别的格式单元类似的格式单元,但参数(作为函数的输入,而不是输出)不能是指针,而只是值。它返回一个新的 Python 对象,适合从 Python 调用的 C 函数返回。

PyArg_ParseTuple() 的一个区别:后者要求其第一个参数是一个元组(因为 Python 参数列表在内部始终表示为元组),而 Py_BuildValue() 并不总是构建元组。它只在格式字符串包含两个或多个格式单元时构建元组。如果格式字符串为空,它将返回 None;如果它恰好包含一个格式单元,它将返回该格式单元描述的任何对象。要强制它返回大小为 0 或 1 的元组,请将格式字符串括起来。

示例(左侧为调用,右侧为生成的 Python 值)

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

1.10. 引用计数

在像 C 或 C++ 这样的语言中,程序员负责堆上内存的动态分配和释放。在 C 中,这是使用函数 malloc()free() 完成的。在 C++ 中,运算符 newdelete 用于实现基本相同的功能,我们将在以下讨论中仅限于 C 的情况。

每个使用 malloc() 分配的内存块最终都应该通过对 free() 的一次调用返回到可用内存池。在正确的时间调用 free() 很重要。如果一个块的地址被遗忘,但没有为它调用 free(),它所占用的内存将无法在程序终止之前被重用。这被称为 内存泄漏。另一方面,如果程序对一个块调用了 free(),然后继续使用该块,它会与通过另一个 malloc() 调用重用该块发生冲突。这被称为 使用已释放的内存。它与引用未初始化数据具有相同的负面后果——核心转储、错误结果、神秘崩溃。

内存泄漏的常见原因是代码中的异常路径。例如,一个函数可能会分配一个内存块,进行一些计算,然后再次释放该块。现在,对函数需求的更改可能会在计算中添加一个测试,以检测错误条件并可以从函数中提前返回。在进行这种提前退出时,很容易忘记释放分配的内存块,尤其是在之后将它添加到代码中时。这种泄漏一旦被引入,通常会很长时间才被发现:错误退出只在所有调用中的一小部分中发生,而且大多数现代机器都有大量的虚拟内存,因此泄漏只会在长时间运行的进程中频繁使用泄漏函数时才会显现出来。因此,通过制定编码规范或策略来最大限度地减少此类错误,从而防止泄漏的发生非常重要。

由于 Python 大量使用 malloc()free(),它也需要一种策略来避免内存泄漏以及使用已释放的内存。所选方法称为 引用计数。原理很简单:每个对象都包含一个计数器,当对该对象的引用存储在某个地方时,该计数器会增加,当对该对象的引用被删除时,该计数器会减少。当计数器达到零时,对该对象的最后一个引用已被删除,并且该对象被释放。

另一种策略称为 自动垃圾回收。(有时,引用计数也被称为垃圾回收策略,因此我使用“自动”来区分两者。)自动垃圾回收的最大优势是用户不需要显式调用 free()。(另一个声称的优势是速度或内存使用量的提高——但这并非硬性事实。)缺点是,对于 C 来说,没有真正可移植的自动垃圾回收器,而引用计数可以实现可移植性(只要函数 malloc()free() 可用——这是 C 标准保证的)。也许有一天,C 会出现一个足够可移植的自动垃圾回收器。在此之前,我们只能忍受引用计数。

虽然 Python 使用传统的引用计数实现,但它也提供了一个循环检测器来检测引用循环。这使得应用程序不必担心创建直接或间接的循环引用;这是仅使用引用计数实现的垃圾收集的弱点。引用循环由包含(可能是间接的)对自身的引用的对象组成,因此循环中的每个对象都有一个非零的引用计数。典型的引用计数实现无法回收属于引用循环中任何对象的内存,或者从循环中的对象引用的内存,即使没有对循环本身的进一步引用。

循环检测器能够检测垃圾循环并回收它们。 gc 模块提供了一种运行检测器的方法(collect() 函数),以及配置接口和在运行时禁用检测器的能力。

1.10.1. Python 中的引用计数

有两个宏,Py_INCREF(x)Py_DECREF(x),它们处理引用计数的增加和减少。 Py_DECREF() 还在计数达到零时释放对象。为了灵活,它不会直接调用 free() - 相反,它通过对象 类型对象中的函数指针进行调用。为此目的(以及其他目的),每个对象还包含一个指向其类型对象的指针。

现在最大的问题仍然是:何时使用 Py_INCREF(x)Py_DECREF(x)?让我们首先介绍一些术语。没有人“拥有”对象;但是,你可以 拥有对对象的引用。对象的引用计数现在定义为对它的拥有引用的数量。引用所有者负责在不再需要引用时调用 Py_DECREF()。引用所有权可以转移。有三种方法可以处理拥有引用:传递它、存储它或调用 Py_DECREF()。忘记处理拥有引用会导致内存泄漏。

也可以 借用 [2] 对对象的引用。引用借用者不应调用 Py_DECREF()。借用者不得比借用它的所有者更长时间地持有对象。在所有者处理完借用引用后使用它会导致使用已释放的内存,应该完全避免 [3]

借用引用比拥有引用的优势在于你不需要在代码中的所有可能路径上处理引用处理 - 换句话说,使用借用引用,你不会在过早退出时冒泄漏的风险。借用引用比拥有引用的劣势在于,在一些微妙的情况下,看似正确的代码可能会在借用它的所有者实际上已经处理了它之后使用借用引用。

通过调用 Py_INCREF(),可以将借用引用更改为拥有引用。这不会影响借用引用的所有者的状态 - 它会创建一个新的拥有引用,并赋予完整的拥有者责任(新的所有者必须正确处理引用,以及以前的所有者)。

1.10.2. 所有权规则

每当将对象引用传递到函数中或从函数中传递出去时,它是否是函数接口规范的一部分,取决于所有权是否随引用一起传递。

大多数返回对象引用的函数会将所有权与引用一起传递。特别是,所有其功能是创建新对象的函数,例如 PyLong_FromLong()Py_BuildValue(),将所有权传递给接收方。即使对象实际上不是新的,您仍然会收到对该对象的新的引用的所有权。例如,PyLong_FromLong() 会维护一个流行值的缓存,并可以返回对缓存项的引用。

许多从其他对象中提取对象的函数也会将所有权与引用一起传递,例如 PyObject_GetAttrString()。然而,情况并不那么明朗,因为一些常见的例程是例外:PyTuple_GetItem()PyList_GetItem()PyDict_GetItem()PyDict_GetItemString() 都返回从元组、列表或字典中借用的引用。

函数 PyImport_AddModule() 也返回一个借用的引用,即使它实际上可能创建了它返回的对象:这是可能的,因为对该对象的拥有引用存储在 sys.modules 中。

当您将对象引用传递到另一个函数时,通常情况下,该函数会从您那里借用该引用——如果它需要存储该引用,它将使用 Py_INCREF() 成为独立的所有者。此规则只有两个重要的例外:PyTuple_SetItem()PyList_SetItem()。这些函数会接管传递给它们的项目的拥有权——即使它们失败了!(请注意,PyDict_SetItem() 及其同类不会接管所有权——它们是“正常的”。)

当从 Python 调用 C 函数时,它会从调用者那里借用其参数的引用。调用者拥有对该对象的引用,因此借用引用的生命周期在函数返回之前一直得到保证。只有当这样的借用引用必须被存储或传递时,才必须通过调用 Py_INCREF() 将其转换为拥有引用。

从 Python 调用的 C 函数返回的对象引用必须是拥有引用——所有权从函数转移到其调用者。

1.10.3. 薄冰

在一些情况下,看似无害地使用借用引用会导致问题。这些问题都与解释器的隐式调用有关,这会导致引用的所有者处置它。

第一个也是最重要的需要了解的情况是在借用对列表项的引用时,对无关对象使用 Py_DECREF()。例如

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* BUG! */
}

此函数首先借用对 list[0] 的引用,然后用值 0 替换 list[1],最后打印借用引用。看起来无害,对吧?但事实并非如此!

让我们跟踪控制流进入 PyList_SetItem()。列表拥有对所有其项目的引用,因此当项目 1 被替换时,它必须处理掉原始的项目 1。现在假设原始项目 1 是用户定义类的实例,并且假设该类定义了 __del__() 方法。如果此类实例的引用计数为 1,则处理它将调用其 __del__() 方法。

由于它是用 Python 编写的,因此 __del__() 方法可以执行任意 Python 代码。它是否可以做一些事情来使 bug() 中对 item 的引用失效?当然可以!假设传递给 bug() 的列表对 __del__() 方法是可访问的,它可以执行一个类似于 del list[0] 的语句,并且假设这是对该对象的最后一个引用,它将释放与之关联的内存,从而使 item 无效。

一旦你知道了问题的根源,解决方案就很容易:暂时增加引用计数。该函数的正确版本如下所示

void
no_bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    Py_INCREF(item);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0);
    Py_DECREF(item);
}

这是一个真实的故事。旧版本的 Python 包含此错误的变体,有人在 C 调试器中花费了大量时间来找出为什么他的 __del__() 方法会失败……

借用引用的第二个问题是涉及线程的变体。通常,Python 解释器中的多个线程不会互相干扰,因为有一个全局锁保护着 Python 的整个对象空间。但是,可以使用宏 Py_BEGIN_ALLOW_THREADS 暂时释放此锁,并使用 Py_END_ALLOW_THREADS 重新获取它。这在阻塞 I/O 调用周围很常见,以便在等待 I/O 完成时让其他线程使用处理器。显然,以下函数与前面的函数具有相同的问题

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...some blocking I/O call...
    Py_END_ALLOW_THREADS
    PyObject_Print(item, stdout, 0); /* BUG! */
}

1.10.4. 空指针

通常,将对象引用作为参数的函数不希望你传递 NULL 指针,并且如果你这样做,它们将转储核心(或导致以后转储核心)。返回对象引用的函数通常只返回 NULL 来表示发生了异常。不测试 NULL 参数的原因是函数通常将它们接收到的对象传递给其他函数——如果每个函数都测试 NULL,就会有很多冗余测试,代码运行速度会更慢。

最好只在“源”处测试 NULL:当接收可能为 NULL 的指针时,例如,来自 malloc() 或可能引发异常的函数。

Py_INCREF()Py_DECREF() 不检查 NULL 指针——但是,它们的变体 Py_XINCREF()Py_XDECREF() 会检查。

用于检查特定对象类型的宏(Pytype_Check())不会检查 NULL 指针——同样,有很多代码会连续调用其中几个来测试对象是否符合各种不同的预期类型,这会导致冗余测试。没有包含 NULL 检查的变体。

C 函数调用机制保证传递给 C 函数的参数列表(示例中的 args)永远不会是 NULL——事实上,它保证它始终是一个元组 [4]

NULL 指针“泄露”到 Python 用户是一个严重错误。

1.11. 用 C++ 编写扩展模块

可以用 C++ 编写扩展模块。有一些限制。如果主程序(Python 解释器)由 C 编译器编译和链接,则不能使用具有构造函数的全局或静态对象。如果主程序由 C++ 编译器链接,则这不是问题。将由 Python 解释器调用的函数(特别是模块初始化函数)必须使用 extern "C" 声明。不需要将 Python 头文件包含在 extern "C" {...} 中——如果定义了符号 __cplusplus(所有最新的 C++ 编译器都定义了此符号),它们已经使用这种形式。

1.12. 为扩展模块提供 C API

许多扩展模块只是提供新的函数和类型供 Python 使用,但有时扩展模块中的代码对其他扩展模块也很有用。例如,一个扩展模块可以实现一个“集合”类型,它类似于无序列表。就像标准 Python 列表类型有一个 C API 允许扩展模块创建和操作列表一样,这个新的集合类型应该有一组 C 函数,用于从其他扩展模块直接操作。

乍一看这似乎很容易:只需编写函数(当然,不要将它们声明为 static),提供一个合适的头文件,并记录 C API。事实上,如果所有扩展模块始终与 Python 解释器静态链接,这将起作用。但是,当模块用作共享库时,一个模块中定义的符号可能对另一个模块不可见。可见性的细节取决于操作系统;一些系统对 Python 解释器和所有扩展模块使用一个全局命名空间(例如 Windows),而另一些系统则需要在模块链接时显式列出导入的符号(AIX 就是一个例子),或者提供不同的策略选择(大多数类 Unix 系统)。即使符号是全局可见的,要调用其函数的模块可能尚未加载!

因此,可移植性要求不要对符号可见性做任何假设。这意味着扩展模块中的所有符号都应该声明为 static,除了模块的初始化函数,以避免与其他扩展模块发生名称冲突(如第 模块的方法表和初始化函数 节中所述)。这意味着应该从其他扩展模块访问的符号必须以不同的方式导出。

Python 提供了一种特殊的机制,用于将 C 级信息(指针)从一个扩展模块传递到另一个扩展模块:胶囊。胶囊是一种 Python 数据类型,它存储一个指针 (void*)。胶囊只能通过其 C API 创建和访问,但它们可以像任何其他 Python 对象一样传递。特别是,它们可以分配给扩展模块命名空间中的一个名称。其他扩展模块可以导入此模块,检索此名称的值,然后从胶囊中检索指针。

Capsule 有多种方法可以用来导出扩展模块的 C API。每个函数都可以拥有自己的 Capsule,或者所有 C API 指针都可以存储在一个数组中,该数组的地址发布在一个 Capsule 中。并且存储和检索指针的各种任务可以在提供代码的模块和客户端模块之间以不同的方式分配。

无论您选择哪种方法,正确命名您的 Capsule 都很重要。函数 PyCapsule_New() 接受一个名称参数 (const char*);您可以传递一个 NULL 名称,但我们强烈建议您指定一个名称。正确命名的 Capsule 提供了一定程度的运行时类型安全性;没有可行的方法可以区分两个未命名的 Capsule。

特别是,用于公开 C API 的 Capsule 应该使用以下约定命名

modulename.attributename

便捷函数 PyCapsule_Import() 使得通过 Capsule 提供的 C API 的加载变得容易,但前提是 Capsule 的名称必须符合此约定。这种行为为 C API 用户提供了高度的确定性,即他们加载的 Capsule 包含正确的 C API。

以下示例演示了一种将大部分负担放在导出模块编写者身上的方法,这适用于常用的库模块。它将所有 C API 指针(示例中只有一个!)存储在一个 void 指针数组中,该数组成为 Capsule 的值。与模块相对应的头文件提供了一个宏,该宏负责导入模块并检索其 C API 指针;客户端模块只需要在访问 C API 之前调用此宏。

导出模块是对 A Simple Example 部分中的 spam 模块的修改。函数 spam.system() 不直接调用 C 库函数 system(),而是调用函数 PySpam_System(),该函数当然会在现实中执行更复杂的操作(例如,在每个命令中添加“spam”)。此函数 PySpam_System() 也导出到其他扩展模块。

函数 PySpam_System() 是一个普通的 C 函数,声明为 static,就像其他所有内容一样

static int
PySpam_System(const char *command)
{
    return system(command);
}

函数 spam_system() 以一种微不足道的方式进行了修改

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return PyLong_FromLong(sts);
}

在模块的开头,紧接在行

#include <Python.h>

之后必须添加两行

#define SPAM_MODULE
#include "spammodule.h"

#define 用于告诉头文件它被包含在导出模块中,而不是客户端模块中。最后,模块的初始化函数必须负责初始化 C API 指针数组

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;

    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Create a Capsule containing the API pointer array's address */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

    if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) {
        Py_XDECREF(c_api_object);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

请注意,PySpam_API 被声明为 static;否则,当 PyInit_spam() 终止时,指针数组将消失!

大部分工作都在头文件 spammodule.h 中,它看起来像这样

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Header file for spammodule */

/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* Total number of C API pointers */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* This section is used in modules that use spammodule's API */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* Return -1 on error, 0 on success.
 * PyCapsule_Import will set an exception if there's an error.
 */
static int
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    return (PySpam_API != NULL) ? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

为了访问函数 PySpam_System(),客户端模块只需在其初始化函数中调用函数(或更确切地说是宏)import_spam()

PyMODINIT_FUNC
PyInit_client(void)
{
    PyObject *m;

    m = PyModule_Create(&clientmodule);
    if (m == NULL)
        return NULL;
    if (import_spam() < 0)
        return NULL;
    /* additional initialization can happen here */
    return m;
}

这种方法的主要缺点是文件 spammodule.h 比较复杂。但是,每个导出函数的基本结构都是相同的,因此只需要学习一次。

最后应该提到的是,Capsule 提供了额外的功能,这对于存储在 Capsule 中的指针的内存分配和释放特别有用。详细信息在 Python/C API 参考手册的 Capsules 部分以及 Capsule 的实现(Python 源代码分发中的文件 Include/pycapsule.hObjects/pycapsule.c)中进行了描述。

脚注