1. 使用 C 或 C++ 扩展 Python — Python 文档

来自菜鸟教程
Python/docs/3.8/extending/extending
跳转至:导航、​搜索

1. 用 C 或 C++ 扩展 Python

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

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

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

笔记

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


1.1. 一个简单的例子

让我们创建一个名为 spam(Monty Python 粉丝最喜欢的食物......)的扩展模块,假设我们要创建一个 C 库函数的 Python 接口 system() 1 . 此函数将一个以空字符结尾的字符串作为参数并返回一个整数。 我们希望这个函数可以从 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> 和 [ X165X]。 如果后一个头文件在您的系统中不存在,它会直接声明函数 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() 如果所有参数都具有正确的类型并且其组件已存储在其地址被传递的变量中,则返回真(非零)。 如果传递了无效的参数列表,则返回 false(零)。 在后一种情况下,它还会引发适当的异常,因此调用函数可以立即返回 NULL(如我们在示例中看到的)。


1.2. 间奏曲:错误和异常

Python 解释器中的一个重要约定如下:当函数失败时,它应该设置异常条件并返回错误值(通常是 NULL 指针)。 异常存储在解释器内部的静态全局变量中; 如果此变量为 NULL,则不会发生异常。 第二个全局变量存储异常的“关联值”(raise 的第二个参数)。 第三个变量包含堆栈回溯,以防错误源自 Python 代码。 这三个变量是 sys.exc_info() 的 Python 中结果的 C 等效项(请参阅 Python 库参考中关于模块 sys 的部分)。 了解它们以了解错误是如何传递的很重要。

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 或 [ X166X])。 它应该 not 调用 PyErr_* 函数之一——一个已经被 g 调用。 f 的调用者应该也返回一个错误指示给 its 调用者,同样 没有 调用 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() 调用)!

选择引发哪个异常完全由您决定。 有预先声明的C对象对应所有内置的Python异常,例如PyExc_ZeroDivisionError,你可以直接使用。 当然,您应该明智地选择例外情况——不要使用 PyExc_TypeError 来表示无法打开文件(应该是 PyExc_IOError)。 如果参数列表有问题,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),在 Built 中描述-in 异常

还要注意 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_KEYWORDS0 的值意味着使用了 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(),它返回一个模块对象,并根据表([X219X 的数组)将内置函数对象插入到新创建的模块中]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 中的新功能),其中从 PyInit_spam 返回 PyModuleDef 结构,并创建模块的一部分留给进口机器。 有关多阶段初始化的详细信息,请参阅 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 解析器——如果您有兴趣,请查看 -c 命令行选项的实现 [ X186X] 来自 Python 源代码。)

调用 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 指针时是安全的(但请注意 [X178X ]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()-ed。

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 参考手册的 Parsing arguments and building values 中有解释。 其余参数必须是其类型由格式字符串确定的变量的地址。

请注意,虽然 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(),它需要一种策略来避免内存泄漏以及使用已释放的内存。 选择的方法称为引用计数。 原理很简单:每个对象都包含一个计数器,当对该对象的引用存储在某处时该计数器递增,当对该对象的引用被删除时该计数器递减。 当计数器达到零时,对对象的最后一个引用已被删除,对象被释放。

另一种策略称为 自动垃圾收集 。 (有时,引用计数也被称为垃圾收集策略,因此我使用“自动”来区分两者。)自动垃圾收集的一大优点是用户不需要调用 [X231X ] 明确。 (另一个声称的优点是速度或内存使用率的提高——但这不是硬性事实。)缺点是对于 C 来说,没有真正可移植的自动垃圾收集器,而引用计数可以可移植地实现(只要函数malloc()free() 可用 - C 标准保证)。 也许有一天会为 C 提供足够便携的自动垃圾收集器。 在那之前,我们将不得不忍受引用计数。

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

循环检测器能够检测垃圾循环并回收它们。 gc 模块公开了一种运行检测器的方法(collect() 函数),以及配置接口和在运行时禁用检测器的能力。 循环检测器被视为可选组件; 虽然它默认包含在内,但可以在构建时使用 --without-cycle-gc 选项在 Unix 平台(包括 Mac OS X)上的 configure 脚本禁用。 如果以这种方式禁用循环检测器,则 gc 模块将不可用。

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()和[ X172X]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 包含此错误的变体,有人在 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++ 编译器链接的,这不是问题。 必须使用 extern "C" 声明将由 Python 解释器调用的函数(特别是模块初始化函数)。 没有必要将 Python 头文件包含在 extern "C" {...} 中——如果定义了符号 __cplusplus(所有最近的 C++ 编译器都定义了这个符号),它们已经使用了这种形式。


1.12. 为扩展模块提供 C API

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

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

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

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

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(),这当然会在现实中做一些更复杂的事情(比如添加“垃圾邮件”到每个命令)。 该功能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 部分和 Capsules 的实现中进行了描述(Python 源代码分发中的文件 Include/pycapsule.hObjects/pycapsule.c) .

脚注

1
这个函数的接口已经存在于标准模块 os 中——它被选为一个简单直接的例子。
2
“借用”参考文献的比喻并不完全正确:所有者仍然拥有参考文献的副本。
3
检查引用计数是否至少为 1 不起作用 — 引用计数本身可能在已释放的内存中,因此可能会被另一个对象重用!
4
当您使用“旧”风格的调用约定时,这些保证不成立——这仍然可以在许多现有代码中找到。