使用 C 或 C++ 扩展 Python

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

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

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

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

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。)

我们文件的第一行可以是:

复制代码
#include \<Python.h\>

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

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

除标准头文件中定义的符号外,由 定义的所有用户可见符号均Python.h具有前缀Py或 。PY为了方便起见,并且由于 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 中的参数列表(例如,单个表达式)到传递给 C 函数的参数有一个简单的转换。C 函数始终有两个参数,通常命名为selfargs"ls -l"

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

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

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

1.2. 间奏曲:错误和异常

整个Python解释器的一个重要约定如下:当函数失败时,它应该设置异常条件并返回错误值(通常是NULL 指针)。异常存储在解释器内的静态全局变量中;如果此变量为NULL, 则不会发生异常。第二个全局变量存储异常的"关联值"( 的第二个参数raise)。第三个变量包含堆栈回溯,以防错误源自 Python 代码。这三个变量是 Python 中结果的 C 等效项(请参阅Python 库参考中sys.exc_info()有关模块的部分)。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 或)。它 应该调用其中一个函数------其中一个函数已经被g 调用过。然后, f 的调用者也应该向其调用 者返回错误指示,同样无需 调用-1``PyErr_*()``PyErr_*()等等------最详细的错误原因已经由最先检测到该错误的函数报告。一旦错误到达 Python 解释器的主循环,就会中止当前正在执行的 Python 代码,并尝试查找由 Python 程序员指定的异常处理程序。

(在某些情况下,模块实际上可以通过调用另一个PyErr_*()函数来给出更详细的错误消息,在这种情况下这样做是可以的。但是,作为一般规则,这是没有必要的,并且可能会导致有关原因的信息要丢失的错误的数量:大多数操作可能会因各种原因而失败。)

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

每个失败的malloc()调用都必须转换为异常 - malloc()(或realloc()) 的直接调用者必须调用 PyErr_NoMemory()并返回失败指示器本身。所有对象创建函数(例如,PyLong_FromLong())都已执行此操作,因此本注释仅与malloc()直接调用的函数相关。

另请注意,除了 和 等重要例外之外,返回整数状态的函数通常在成功和失败时PyArg_ParseTuple()返回正值或零,就像 Unix 系统调用一样。-1

最后,当您返回错误指示符时,请小心清理垃圾(通过创建Py_XDECREF()Py_DECREF()调用已创建的对象)!

引发哪种异常的选择完全由您决定。所有内置Python异常都有对应的预先声明的C对象,例如 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_INCREF(SpamError);
    PyModule_AddObject(m, "error", SpamError);
    return m;
}

请注意,异常对象的 Python 名称是spam.error。该 PyErr_NewException()函数可以创建一个基类为的类Exception(除非传入另一个类而不是NULL ),如内置异常中所述。

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

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

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

复制代码
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);

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

复制代码
return PyLong_FromLong(sts);

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

如果您的 C 函数不返回有用的参数(返回 的函数 void),则相应的 Python 函数必须返回None。您需要这个习惯用法来执行此操作(由宏实现Py_RETURN_NONE ):

复制代码
Py_INCREF(Py_None);
return Py_None;

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

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

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

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

请注意第三个条目 ( METH_VARARGS)。这是一个标志,告诉解释器 C 函数要使用的调用约定。通常应该始终是METH_VARARGSor ;值表示使用了过时的变体。METH_VARARGS | METH_KEYWORDS``0PyArg_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 将函数声明为返回类型,声明平台所需的任何特殊链接声明,对于 C++,将函数声明为.PyObject *``extern "C"

当Python程序spam第一次导入模块时, PyInit_spam()被调用。(请参阅下面有关嵌入 Python 的注释。)它调用,返回一个模块对象,并根据模块定义中找到的 PyModule_Create()表(结构数组)将内置函数对象插入到新创建的模块中。返回指向它创建的模块对象的指针。对于某些错误,它可能会因致命错误而中止,或者如果模块无法令人满意地初始化,则返回NULL 。 init 函数必须将模块对象返回给其调用者,以便将其插入到.PyMethodDefPyModule_Create()sys.modules

嵌入 Python 时,PyInit_spam()除非表中存在条目,否则不会自动调用该函数PyImport_Inittab。要将模块添加到初始化表,请使用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 \*/
    PyImport_AppendInittab("spam", PyInit_spam);

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

    /\* Initialize the Python interpreter. Required. \*/
    Py_Initialize();

    /\* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. \*/
    PyImport_ImportModule("spam");

    ...

    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 来重建解释器。您还可以在子目录中运行make Modules/ ,但必须首先Makefile通过运行" make Makefile"在那里重建。(每次更改文件时都需要这样做 Setup。)

如果您的模块需要链接其他库,这些库也可以列在配置文件中的行上,例如:

复制代码
spam spammodule.o -lX11

1.6. 从 C 调用 Python 函数

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

幸运的是,Python解释器很容易递归调用,并且有一个标准的接口来调用Python函数。(我不会详细讨论如何使用特定字符串作为输入来调用 Python 解析器 - 如果您感兴趣,请查看Python 源代码中-c命令行选项 的实现。)Modules/main.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()就其参数而言是"引用计数中立"的。在示例中,创建了一个新元组作为参数列表,该列表 在调用Py_DECREF()后立即被 -ed PyObject_CallObject()

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

然而,在执行此操作之前,检查返回值是否不为NULL 非常重要。如果是,Python 函数会引发异常而终止。如果调用的 C 代码PyObject_CallObject()是从 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 函数的参数列表。格式参数必须是格式字符串,其语法在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[], ...);

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

使用关键字参数时无法解析嵌套元组!传入的关键字参数不存在于kwlist 中将导致TypeError引发。

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

复制代码
#include "Python.h"

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    char *state = "a stiff";
    char *action = "voom";
    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)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()and free(),它需要一种策略来避免内存泄漏以及使用已释放的内存。所选择的方法称为引用计数。原理很简单:每个对象都包含一个计数器,当对该对象的引用存储在某处时,该计数器会递增;而当对该对象的引用被删除时,该计数器会递减。当计数器达到零时,对该对象的最后一个引用已被删除,并且该对象被释放。

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

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

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

1.10.1. Python 中的引用计数

有两个宏Py_INCREF(x)Py_DECREF(x),它们处理引用计数的递增和递减。Py_DECREF()当计数达到零时也会释放该对象。为了灵活性,它不直接调用 ,而是通过对象类型 object free()中的函数指针进行调用。为了这个目的(和其他目的),每个对象还包含一个指向其类型对象的指针。

现在最大的问题仍然是:何时使用Py_INCREF(x)and 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代码。item它可能会做一些事情来使对in的引用无效吗 bug()?你打赌!假设传入的列表 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() 会检查 NULL 指针。

用于检查特定对象类型的宏 ( Pytype_Check()) 不检查NULL 指针 --- 同样,有很多代码连续调用其中的几个宏来针对各种不同的预期类型测试对象,这会生成冗余测试。没有带有NULL 检查的变体。

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

让NULL指针"逃逸"给 Python 用户是一个严重的错误。

1.11. 用 C++ 编写扩展

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

1.12. 为扩展模块提供 C API

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

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

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

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

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

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

特别是,用于公开 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部分中的模块 的修改。该函数并不直接调用C库函数,而是调用一个函数 ,这在现实中当然会做一些更复杂的事情(例如在每个命令中添加"spam")。该功能 还导出到其他扩展模块。spam.system()``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 (c_api_object != NULL)
        PyModule_AddObject(m, "_C_API", c_api_object);
    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相当复杂。然而,每个导出函数的基本结构都是相同的,因此只需学习一次。

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

相关推荐
骑个小蜗牛几秒前
Python 标准库:random——随机数
python
薄荷故人_5 分钟前
从零开始的C++之旅——红黑树封装map_set
c++
Trouvaille ~10 分钟前
【机器学习】从流动到恒常,无穷中归一:积分的数学诗意
人工智能·python·机器学习·ai·数据分析·matplotlib·微积分
是十一月末27 分钟前
Opencv实现图像的腐蚀、膨胀及开、闭运算
人工智能·python·opencv·计算机视觉
悲伤小伞33 分钟前
C++_数据结构_详解二叉搜索树
c语言·数据结构·c++·笔记·算法
云空34 分钟前
《探索PyTorch计算机视觉:原理、应用与实践》
人工智能·pytorch·python·深度学习·计算机视觉
dowhileprogramming1 小时前
Python 中的迭代器
linux·数据库·python
m0_675988232 小时前
Leetcode3218. 切蛋糕的最小总开销 I
c++·算法·leetcode·职场和发展
0zxm2 小时前
08 Django - Django媒体文件&静态文件&文件上传
数据库·后端·python·django·sqlite
灰太狼不爱写代码4 小时前
CUDA11.4版本的Pytorch下载
人工智能·pytorch·笔记·python·学习