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 代码交互,并且在 Python 的不同实现之间比编写和编译 C 扩展模块更具可移植性。

1.1. 一个简单的例子

让我们创建一个名为 spam 的扩展模块(Monty Python 粉丝最喜欢的食物……),假设我们要为 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

#define PY_SSIZE_T_CLEAN 用于指示在某些 API 中应使用 Py_ssize_t 而不是 int。自 Python 3.13 以来,这不是必需的,但我们在此处保留它以实现向后兼容。有关此宏的描述,请参阅 字符串和缓冲区

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() 返回 true(非零)。如果传递了无效的参数列表,则返回 false(零)。在后一种情况下,它还会引发一个适当的异常,以便调用函数可以立即返回 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);
    if (PyModule_AddObjectRef(m, "error", SpamError) < 0) {
        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 指针,正如我们所见,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。初始化函数必须将其模块对象返回给调用方,以便随后将其插入到 sys.modules 中。

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    /* 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 */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }

    /* Initialize the Python interpreter.  Required.
       If this step fails, it will be a fatal error. */
    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);

    /* 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");
    }

    // ... use Python C API here ...

    return 0;

  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}

注意

sys.modules 中删除条目或将编译后的模块导入进程中的多个解释器(或在没有中间 exec() 的情况下执行 fork() 之后)可能会给某些扩展模块带来问题。扩展模块作者在初始化内部数据结构时应谨慎行事。

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
#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 * const *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
#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] 的引用,然后将 list[1] 替换为值 0,最后打印借用的引用。看起来无害,对吧?但事实并非如此!

让我们跟踪控制流进入 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 包含此 bug 的变体,有人花了很多时间在 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:当接收到可能为 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 级别的信息(指针)从一个扩展模块传递到另一个扩展模块:Capsules(胶囊)。Capsule 是一种 Python 数据类型,它存储一个指针 (void*)。Capsule 只能通过其 C API 创建和访问,但可以像任何其他 Python 对象一样传递。特别是,它们可以被赋值给扩展模块命名空间中的一个名称。其他扩展模块随后可以导入该模块,检索该名称的值,然后从 Capsule 中检索指针。

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

无论您选择哪种方法,正确命名 Capsule 都非常重要。函数 PyCapsule_New() 接受一个 name 参数 (const char*);允许传入 NULL 名称,但我们强烈建议您指定一个名称。正确命名的 Capsule 提供了一定程度的运行时类型安全;没有可行的方法来区分一个未命名的 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 之前调用此宏即可。

导出模块是对 一个简单的例子 部分中的 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_Add(m, "_C_API", c_api_object) < 0) {
        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 相当复杂。但是,每个导出的函数的基本结构是相同的,因此只需要学习一次。

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

脚注