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

  1. 使用 C 或 C++ 扩展

要是你掌握了C, 增添新的内置模块就会蛮简单。有两件没办法直接去做的事, 能够借助它来达成: 达成新的内置对象类型;调用C的库函数以及系统调用。

为了达成支持扩展这一目的, API(也就是应用程序编程接口)界定了一系列的函数、宏以及变量, 借助这些能够访问运行时系统的大部分内容。该API能够通过在一个C源文件当中引用".h"头文件这种方式来加以使用。

编写扩展模块的方式, 取决于你的目的,以及系统设置, 下面章节会详细介绍。

备注

C扩展接口所专门指的情况是, 扩展模块在别的实现上面是没办法工作的。在多数情形之下, 为了维持可移植性, 是应当避免去写C扩展的。比如说, 要是你的用例调用了C库或者系统调用, 那你就应当考虑去使用模块或者cffi库, 而并非自己去写C代码。这些模块能让你编写代码以与C代码进行接口对接, 而且和编写并编译C扩展模块相比, 这种方法在不同实现之间有着更高的可移植性。

1.1. 一个简单的例子

那咱们来搞一个扩展模块spam, 它是Monty粉丝最为钟爱的食物相关的(此处省略部分描述), 而且呢, 还想着去构建一下那种对应C库函数()的接口。这个库函数, 它会接纳一个以null作为结尾标识的字符串类型的参数, 之后返回的是一个整数类型的数据。我们期望能够在(此处省略)里, 按照这样的一种方式去调用这个函数:

复制代码
>>> import spam
>>> status = spam.system("ls -l")

最先去创建一个.c文件, 传统说来要是存在一个模块被称作spam, 那样与之对应的用来实现它的C文件就被叫做.c, 要是这个的模块名字是非常长的情况, 例如, 那么这个模块的文件能够直接被叫做.c。

文件中开始的两行是:

复制代码
#define PY_SSIZE_T_CLEAN
#include 

现将会进行导入操作, 针对API, 要是你心里乐意, 能够在此处增添那用以描述模块目标以及版权信息的注释。

备注

鉴于, 存在这样的可能性, 即会去定义一些, 能够在某些系统之上, 对标准头文件产生影响的, 预处理器定义, 所以, 在包含任何标准头文件以前, 你务必, 要先包含, .h。

被用以表明, 应当于部分 API 里替代 int 来运用。它自 3.13 起便已无须使用, 然而我们予以保留, 以便实现向下兼容。请参考获取该宏的描述。

为用户所能够看见的、由.h进行定义的那些符号, 全都带有Py或者PY这样的前缀, 不过呢, 仅仅只有在标准头文件里被定义的符号是除外的。

小技巧

为了维持向下兼容性, .h涵盖了一些标准头文件, C扩展理应包含其要运用的标准头文件, 而不是依赖这些隐含的包含。要是运用受限C API 3.13版或更新版本, 隐含的包含如下:

如若处于未定义的状况, 或者被设置成为3.12版及相比于3.12版更旧的版本, 那么还会涵盖如下这般样式的头文件:

要是处于未定义的状况, 或者被设置成为3.10版以及比3.10版更旧的版本, 那么还会包含像下面这样的头文件:

往下面添加C函数至扩展模块, 当对spam.()进行调用的时候会产生响应, (我们在稍后的时候会看到相应的调用):

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

有种将参数列表直接进行翻译的办法, 比如说, 单独的"ls -l" , 作为要传递给C函数的参数。而C函数一直都有两个参数, 常见的名字是self和args呐。

对于模块级函数而言, self 参数所指向的是模块对象, 而对于方法来说, self 参数指向的是对象实例。

args参数, 是指向一个tuple对象的指针, 该tuple对象包含参数, 每个tuple项对应一个调用参数, 这些参数全都是对象, 要在我们的C函数中使用它们, 就需要先将其转换为C值, API中的函数会检查参数类型并将其转换为C值, 它使用模板字符串确定需要的参数类型以及存储被转换的值的C变量类型, 细节将稍后说明。

当处在把组成部分依序置于传进来地址里的时候, 并且全部参数有着正确类型, 此时作出的返回值, 是那个代表为真(不为零)这一情况值;而出现传入无效参数这种状况时, 它作出的返回值则是代表为假(值是零)这种情况的值;在后续所举例子当中, 还会出现抛出特定异常的情形, 这会致使调用的函数能够明白返回的是NULL(此即例子当中所看到的那样)。

1.2. 关于错误和异常

整体而言, 解释器系统存在一个重要惯例, 具体如下: 一旦函数遭遇运行失败的情况, 那么它就应该去设置一个异常条件, 进而返回一个错误值, 此错误值一般为 -1 或者 NULL 指针。异常信息存放于解释器线程状态的三个成员之中。要是不存在异常, 那么这三个成员的值便为 NULL。而在其他状况下, 它们属于所返回元组的成员的 C 对应物。这三个成员分别是异常类型、异常实例以及回溯对象。对于理解错误究竟是怎样被传递的而言, 理解它们是极为重要的。

API中定义了一些函数来设置这些变量。

最常被使用的便是, 其参数分别是异常对象以及C字符串, 异常对象通常是类似这样的预定义对象, C字符串会指明异常的原因, 并且会被转化为一个字符串对象存储成为异常的"关联值"。

又有一个具备用处的函数是, 它仅仅接纳一个异常对象, 而异常的描述是被涵盖于全局变量errno之中的。存在那却是最为通用的函数, 它涵盖着两个参数, 其中一个是异常对象, 另一个是异常描述。你并不需要去运用它来提升传递至其他函数的参数对象的引用计数。

你可经由, 在不致使破坏存在的情形下, 检测有无异常被设定。这会回返当下的异常对象, 不然的话, 若未出现异常, 便会回返NULL。你寻常是无需调用()去查看于函数调用里有无错误出现的情形的, 鉴于你理应能够从返回值当中察觉此点。

在一个函数 f 调用另一个函数 g 的时候, 检测到后者出现了错误, 此时 f 应当自行返回一个错误值, 这个错误值通常是 NULL 或者 -1。它不应该去调用某个特定的函数, 因为这类函数已经被 g 调用过了。f 的调用者接下来也应当返回一个错误去提示它的调用者, 同样不应该调用那个特定的函数, 按照这样的方式类推下去, 因为错误的最详细原因已经由首先检测到它的函数报告过了。这个错误一旦抵达解释器的主循环, 当前执行中的代码就会被中止, 然后去尝试找出程序员指定的异常处理器。

有些情形下, 模块的确能够借助调用别的函数, 给出更为详尽的错误消息, 而且在这些情形下, 是能够这么做的。然而依照一般规则, 这并无必要, 还可能致使有关错误的信息遗失: 绝大多数操作会出于各种各样的缘由而失败。

想去忽略经由一个失败的函数调用所设定的异常, 针对异常条件通过调用, 需要显式地去将其清除。C代码应当去进行()调用的唯一情形是要是它不打算把错误传递给解释器而计划完全凭借自身来处理这个错误(也许是尝试别的方法, 又或者是装作没有出现错误)。

每一回失败的, 那()调用都得转化成一个异常。()(或者())的直接调用者一定要调用, 以此返回错误来给出提示。全部对象创建函数(比如说)已然这么去做了, 因而这个提示仅仅是用于直接调用()的情形。

要额外留意的是, 除了 等关键的例外情形, 那些返回整数状态码的函数, 一般都是通过返回正值亦或零来表明成功之意, 且是以-1来代表失败, 这正如Unix系统调用那般。

最后, 当你返回一个错误指示器时, 要注意清理垃圾, 这一清理垃圾, 是通过为你已经创建的对象执行操作要么, 要么调用相关内容来达成的!

是你的喜好完全决定选择引发哪一个异常。有对应预声明C对象的均为所有内置异常, 比如, 可以直接使用它们。当然, 异常应当被明智地选择------表示文件无法打开不要用(或许用比较适宜)。函数通常会引发如果参数列表存在问题。适宜使用如果你期望一个参数的值务必在特定范围之内或者务必满足其他条件。

对于你而言, 同样能够去定义属于你所拥有模块自身特有的全新异常情况。要达成这一目标的最为简便的途径是, 于文件的起始之处去声明一个静态的全局对象变量。

复制代码
static PyObject *SpamError = NULL;

并通过在模块的 函数 (()) 中调用 来初始化它:

复制代码
SpamError = PyErr_NewException("spam.error", NULL, NULL);

由于它属于全局变量范畴, 这般一来每次模块重新进行初始化之际它都会被覆盖, 也就是说在函数被调用的那个时候。

在当下, 让咱们避开这个问题, 我们会借由引发, 来防止重复的初始化。

复制代码
static PyObject *SpamError = NULL;
static int
spam_module_exec(PyObject *m)
{
    if (SpamError != NULL) {
        PyErr_SetString(PyExc_ImportError,
                        "cannot initialize spam module more than once");
        return -1;
    }
    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        return -1;
    }
    return 0;
}
static PyModuleDef_Slot spam_module_slots[] = {
    {Py_mod_exec, spam_module_exec},
    {0, NULL}
};
static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_size = 0,  // non-negative
    .m_slots = spam_module_slots,
};
PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

请注意, 该异常对象, 那个名称是spam.error的, 函数能够创建基类是的类, 除非传入了另一个类而非NULL, 就如同中所描述的那般。

请留意, 变量留存了一个针对新缔造的异常类的引用, 这是特意如此的!鉴于异常有可能被外部代码自模块内移除, 所以需持有一个对该类的引用, 用于确保它不会被舍弃, 进而致使成为一个悬空指针。要是异常类变成悬空指针, 那么引发该异常的C代码兴许会造成核心转储或者其他意外的附带影响。

当下, 用于去除该引用的调用处于缺失状态。哪怕是在解释器关闭之际, 全局变量也不会被视作可进行垃圾回收的对象。它将会出现"泄漏"的情况。然而, 我们的确能够确保这种情况在每个进程中最多只会发生一回。

本样例稍后将讨论 作为函数返回类型的用法。

能够于扩展模块里进行调用, 以此来引发 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, 此值乃返回对象指针的函数的错误提示, 这取决于所设置的异常, 在其他情形下, 参数的字符串值会被拷贝至局部变量, 这是一次指针赋值, 且你不应修改它所指向的字符串, 故而在标准C里, 变量应当被正确声明为const char *。

在下一个语句之中, 会运用UNIX系统函数 () , 而传递给它的那种参数 , 乃是方才从 那里所取出来的。

复制代码
sts = system(command);

对于我们所拥有的 spam.() 函数而言, 其返回 sts 的值所采用的形式必须是对象 , 而达成这一要求是借助运用函数予以实现的。

复制代码
return PyLong_FromLong(sts);

在这般情形之下, 会返回一个整数对象, 此对象系在堆当中予以管理。

若是你存在一个并不返回有助于参数的C函数, 也就是那种返回void的函数, 那么与之对应的对应函数必然要返回None, 你得采用这种写法, 它是借着宏来予以实现的。

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

它是同 None 这个特殊对象相对应的 C 名称, 它是一个实实在在的对象, 并非 NULL 指针。我们可以看到, 后者在多数上下文里都表示"错误"的意思, 就是这样。

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

我答应过要给大伙呈现怎样从程序里调用()。其一, 我们理应在"方法表"中将其名和地址做列举:。

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

如果单独去使用, 函数会等待传过来的tuple格式的参数, 并且最终会使用进行解析。

若要把关键字参数传递给该函数, 那么能在第三个字段里设置比特位。在此种情形下, C函数理应接纳第三个*形参, 此形参将会是个由关键字构成的字典。借助来把参数解析成函数。

这个方法表必须被模块定义结构所引用。

复制代码
static struct PyModuleDef spam_module = {
    ...
    .m_methods = spam_methods,
    ...
};

给解释器传递这个结构体, 得在模块的初始化函数里进行。初始化函数得命名为(), 这里面name是模块的名称, 而且它应该是模块文件中定义的唯一非 条目:。

复制代码
PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

注意, 把函数声明成, "* 返回类型", 声明出平台所要求的, 任何特殊链接声明, 并且针对"= C++", 将函数声明为, "C"。

(此处)会在每个解释程序首次引入它的spam模块的时候(此行动)被发起调用。(并且这个步骤)请在参考下文里有关嵌入式方面的阐释说明之后执行。(还要在其中执行这样一个动作即)必须借助特定方式返回一个指向模块定义的指针, (只有这样)以便导入机制能够构建出该模块并将它妥善保存到sys之中。

嵌入之际, 如果表内不存在条目, 若是没有条目在表中, 那么不会自动去调用函数()当处于此情形, 要把相关模块追加至初始化表里头, 可通过选择使用特定方式, 然后能够导入该模块去实现:。

复制代码
#define PY_SSIZE_T_CLEAN
#include 
int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);
    /* 添加一个内置模块,在 Py_Initialize 之前 */
    if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }
    /* 将 argv[0] 传给 Python 解释器 */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    /* 初始化 Python 解释器。 必需的操作。
       如果此步骤失败,将导致致命错误。 */
    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);
    /* 可以选择导入模块;或是作为替代,
       导入可以被延迟直到由嵌入的脚本
       来导入它。 */
    PyObject *pmodule = PyImport_ImportModule("spam");
    if (!pmodule) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'spam'\n");
    }
    // ... 在此使用 Python C API ...
    return 0;
  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}

备注

倘若你去声明一个全局变量, 或是一个局部静态变量, 这时候模块在进行重新初始化时, 就有可能出现那种预料之外的附带影响, 比如说在从sys.里移除条目的时候, 又或者是将已编译的模块导入到一个进程中的多个解释器之时(或者是在未对exec()进行干预的情况下执行fork()之后)。要是模块状态没有被完全 , 那么开发者就应当考虑把模块标记为不支持子解释器(通过 )。

在源代码发布包里面的 /.c 之处, 含有一个更为详细的示例, 这个文件能够被用作代码模板, 也可当作学习样例。

1.5. 编译和链接

在你能够启用新买的升级版扩展以前, 你还要从事两件事项: 运用该系统达到编纂跟链接。要是你采用动态加载方式, 这取决于你所运用的操作系统的动态加载机制;更多详情请查看编译扩展模块的章节处里(该章节内容), 以及在此处编译所需的额外讯息(此章节详情)。

要不使用动态加载, 或是想要让模块永久性成为解释器的一部分, 那就得修改配置设置, 还得重新构建解释器。好在Unix系统上这颇简易, 只需要把你们的相关文件(以.c为例来说)放置在解压缩源码发行包的根目录下, 往 / Setup . local里增添一行内容用以描述你们的文件:

复制代码
spam spammodule.o

接下来, 于顶层的根目录之中, 去运行那make操作, 用以对其解释器进行再度构建。您要是实在有需要的话, 便也能够在那个名为 / 的子目录之下动用make, 只不过有一点至关紧要啊, 事前您必须得先去把相关的文件予以重新建造一番, 而后再运行那标注着 'make ' 的命令才行。(要知道, 您每一回对Setup文件进行哪怕极小的修改操作之后, 都绝对要用这样的一系列操作流程才行呢)

要是你的模块所需的是额外的链接, 那么这些内容能够在配置文件里被列出, 举个例子就是:

复制代码
spam spammodule.o -lX11

1.6. 在C中调用函数

到目前为止, 我们始终都将注意力着重放在了让去调用C函数上面, 实际上反过来也是具备一定用处的, 也就是运用C去调用函数, 这在回调函数当中是特别有用的, 要是一个C接口采用回调的方式, 那么就需要达成这个回调的机制。

幸运的是, 解释器的递归调用具备便利性, 且提供了调用函数的标准接口, (要是你对怎样附带特定字符串作为输入来调用解析器感兴趣, 那就请参阅)

让函数被调用这件事是挺简单的, 首先, 程序得把函数对象给传递过去。这件事得由一个函数(或者是其他的接口)来达成实现。在调用这个函数之际, 要用全局变量去保存函数对象的指针, 同时, 还得调用 () 去把引用计数给增加, 当然, 就算不使用全局变量, 其实也没什么太大的问题。举个例子来说, 像下面这样的函数, 有可能会是模块定义之中的一部分:

复制代码
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);         /* 添加一个指向新回调的引用 */
        Py_XDECREF(my_callback);  /* 丢弃之前的回调 */
        my_callback = temp;       /* 记住新的回调 */
        /* 返回 "None" 的样例 */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

必须使用旗标将此函数注册到解释器, 这会在一节里详细描述, 函数及其参数的文档见于一节。

这两个宏, 其作用是能够增加或者减少一个对象的引用计数, 而且即便存在NULL指针的情况下, 依然能够确保安全, 不过需要注意的是, 在这样的上下文当中, temp它不会是NULL状态, 更多相关方面的信息, 可以去参考某章节。

随后, 当需要去调用这个函数的时候, 你将会去调用C函数。这个函数存在着两个参数, 这两个参数均属于指针, 它们指向任意的对象: 也就是函数, 以及它的参数列表。参数列表必然始终是一个元组对象, 其长度作为参数的个数数量。要是不带参数地去调用函数, 那就传入NULL或者一个空元组;要是带一个参数调用它, 那就传入一个单元组。会在其格式字符串含有一对圆括号内的零个或者多个格式代码时返回一个元组。例如:。

复制代码
int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* 此时将调用回调 */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

返回那个对象指针, 而这同样是函数所具有的返回值 , () 是一个针对其参数"引用计数不相关"的函数 , 例子里面新的元组被创建用来作为参数列表 , 并且在 () 之后紧接着就被使用了。

它的返回值始终是"新"的, 这存在两种情况, 一是返回的是一个崭新创建的对象, 二是返回的是已经存在的对象, 不过其引用计数有所增加。故而, 除非你打算将该结果存储于全局变量之中, 否则你都得对这个值加以使用, 哪怕你对于里面所包含的内容(尤其要强调)并无兴趣。

可是在你开展如此行为之前, 极为关键的一点在于核查返回的值并非为NULL。要是返回值是的话, 那么这个函数将会终止运行, 并且引发异常情况。倘若被调用的C代码是在特定情境中被唤起的, 那它理应立刻返回一个错误信息, 以此告知其调用者, 从而使得解释器能够打印栈回溯数据, 或者让调用方的代码能够对该异常进行处理。要是这无法达成或者不符合本意的话, 那就应当借助调用特定操作来清理异常。举例来说:。

复制代码
if (result == NULL)
    return NULL; /* 回传错误 */
...使用 result...
Py_DECREF(result);

取决于特定的回调函数, 你还得给出一个参数列表至。于某些情形下参数列表是由程序所提供的, 借助接口再传至回调函数对象。如此便能以不变形式直接传递。另外一些时段你得构建一个新的元组来传递参数。最为简便的办法便是 函数构造tuple。举个例子, 你要传递一个事件代码时能够用如下代码:。

复制代码
PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
    return NULL; /* Pass error back */
/* 可以在此使用 result */
Py_DECREF(result);

注意, () 所在之处会马上加以调用, 是在错误检查之前进行的。当然, 还得留意一些常见的错误, 像是, 有可能会碰到内存不足之类的情况。

你呼叫函数之际还得留意, 借由关键字参数予以呼叫, 要能支撑普通参数跟关键字参数。同如上述示例里, 我们运用来构建字典。

复制代码
PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
    return NULL; /* 回传错误 */
/* 可以在此使用 result */
Py_DECREF(result);

1.7. 提取扩展函数的参数

函数 的声明如下:

复制代码
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);

参数arg可要成为包含着从传递给C函数的参数编排的元素所构成的元组样式的对象才行。参数只能硬是变为格式字符串, 其相关语法参照C/API典籍里边所述的那一内容。剩下那些参数均乃是各个变量地址形成之物, 并具备有与格式字符串相匹配的类型。

得留意, 会去检测他所要的参数类型, 然而没办法检测传递到他那里的C变量地址, 要是在这块出问题了, 有可能会在内存当中随机地写入些内容, 要谨慎。

留意, 任何经由调用者所供给的对象引用, 皆是借来的引用, 不要去递减它们的引用计数!

一些调用的例子:

复制代码
#define PY_SSIZE_T_CLEAN
#include 
复制代码
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;
ok = PyArg_ParseTuple(args, ""); /* 无参数 */
    /* Python 调用: f() */
复制代码
ok = PyArg_ParseTuple(args, "s", &s); /* 一个字符串 */
    /* 可能的 Python 调用: f('whoops!') */
复制代码
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* 两个长整型和一个字符串 */
    /* 可能的 Python 调用: f(1, 2, 'three') */
复制代码
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* 一对整数和一个字符串,其大小也将被返回 */
    /* 可能的 Python 调用: f((1, 2), 'three') */
复制代码
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* 一个字符串,并可选择传入另一个字符串和一个整数 */
    /* 可能的 Python 调用:
       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);
    /* 一个矩型和一个点 */
    /* 可能的 Python 调用:
       f(((0, 0), (400, 300)), (10, 10)) */
}
复制代码
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* 一个复数,并提供一个函数名用于错误处理 */
    /* Possible Python call: myfunction(1+2j) */
}

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

函数 声明如下:

复制代码
int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char * const *kwlist, ...);

与函数所定义的一致的是 arg形参,用来标识形参的是作为第三个参数从运行时接收的关键字字典的形参,形参是以NULL结尾的字符串列表,名称从左至右与来自的类型信息相匹配的是形参,如果执行成功会返回真值否则返回假值并引发一个适当异常的是如果执行成功会返回真值否则返回假值并引发一个适当异常。

备注

当使用关键字参数那时, 嵌套着的元组起不了作用, 而不在特定范围里的关键字参数, 会致使出现异常情况。

以下所举的例子, 乃是运用关键字参数的示例模块, 其作者是 Geoff。

复制代码
#define PY_SSIZE_T_CLEAN
#include 
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[] = {
/* 函数的转换是必要的因为 PyCFunction 值
* 仅接受两个 PyObject* 形参,而 keywdarg_parrot()
* 接受三个。
*/
{"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 keywdarg_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "keywdarg",
.m_size = 0,
.m_methods = keywdarg_methods,
};
PyMODINIT_FUNC
PyInit_keywdarg(void)
{
return PyModuleDef_Init(&keywdarg_module);
}

1.9. 构造任意值

这个函数与 很相似,声明如下:

复制代码
PyObject *Py_BuildValue(const char *format, ...);

接纳一个格式字符串, 跟 一样, 然而参数必定要是来源于原变量的地址指针, 是输入传入给函数的, 而不是传出的。最终带来一个适合于返回给C函数调用用的代码的对象。

和 的差异在于, 后续所需的要求会返回一个元组(参数据通常在内部被描述成元组), 像是用于当作参数传递给别的函数。 并非一直生成元组, 当格式字符串多于1个时会生成元组, 要是格式字符串为空则返回None, 若只有一个参数则直接返回该参数对象。要是要求强制生成一个长度为0的元组, 或者包含一个元素的元组, 就得在格式字符串里加上括号。

例子(左侧是调用,右侧是值结果):

复制代码
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++语言范畴内, 程序员被赋予了负责动态分配以及回收堆heap之中内存的职责。在C这种程序设计语言里, 借助函数 () 以及 free() 去达成此项操作。处于C++语言环境下, 则是经由操作 new 以及 用来落实相同的功能。

每个借助 () 分配得来的内存块, 最终都理应经由恰好一回对 free() 的调用, 返还至可用内存池之中。而调用 free() 的那个时机, 实则相当关键。要是一个块地址出现被遗忘的情况, 并且没有针对它去执行 free() 调用, 那么它所占据的内存, 在程序终结以前, 都将没办法被再度加以使用。这情形便被称作内存泄漏。另一方面, 倘若程序针对一个块地址调用了 free(), 然而后续却依旧持续使用该内存块, 那它将会和通过另外一个 () 调用对该内存块进行的重新使用形成冲突。这便被称作, 对已然释放的内存予以使用。由此引致的后果, 与引用未初始化的数据情况完全相同糟糕, 像是出现核心转储现象, 产生错误结果, 发生意外崩溃等等。

内存泄露常常出现在一些并非常见的代码流程之中。举例来说, 有一个函数, 它申请了内存, 之后进行了一些计算, 随后便释放了内存块。如今, 一些针对函数的修改, 可能会增加对计算的测试, 并且检测错误条件, 接着过早地从函数返回了。这极易导致在退出前忘记释放内存,尤其是后期修改的代码。这种内存泄漏, 一旦被引入, 通常很长时间都难以被检测到, 错误退出被调用的频率较低, 而且现代电脑具备非常巨大的虚拟内存, 所以泄漏唯有在长期运行或者频繁调用泄漏函数时才会变得明显。所以, 需要防止内存泄漏, 借助代码规范以及所出台的种种规则、办法, 去将这类错误控制在最小的范围之内, 这是非常有必要的。

经由 () 以及 free() 涵盖大量的内存分配与释放, 同样得规避内存泄漏与野指针。他所选用的方法便是 引用计数。其原理相对简单: 单个对象均具备一个计数器, 该计数器的增减跟对象引用的增减直接关联, 当引用计数为 0 时, 意味着对象已然不存在存在的意义了, 对象便可被删除了。

你提供的内容存在较多语法错误和表述不清晰的地方, 修正后为: 一个an是用来表示某个事物的, 它也被称为一项(用空格填充), 因此在此使用双引号来分隔这两者。使用an的好处是用户无需进行括号操作, 一个空格占用一定的宽度也可使用(这只是一种宽泛的用法, 并非严格事实)。问题在于对于C语言来说, 没有真正意义上的空, 而对于空格来说它可以被使用(只要该用法有含义而且自由使用------这是c语言所具备的能力)。或许未来会为C语言开发出一种特殊的空格使用方式。在那之前, 我们只能先这样接受它。

采用传统的引用计数方式来实现, 照样提供了循环监测器, 用来检测引用循环。这致使应用不必担忧直接或者间接创建了循环引用, 而这是引用计数垃圾收集的一个薄弱之处。引用循环是对象(有可能直接)引用了自身, 因而循环中的每一个对象的引用计数都并非为0。典型的引用计数实现没办法回收处于引用循环当中的对象, 或者被循环所引用的对象, 即便不存在循环以外的引用了。

垃圾回收循环能遭循环检测器觉察, 且能被其回收, 检测器可借模块给出的一种运行方式(此方式为函数)运行, 模块那里还有多个配置接口跟在运行时刻能禁用该检测器的功能。

1.10.1. 中的引用计数

存在两个宏, 分别是(x)和(x), 它们会对引用计数进行增加或者减少的处理, 也会在引用计数值达到0这个情况时, 去释放对象, 为了具备灵活性, 不会直接去调用free(), 而是借助对象的类型对象的函数指针来进行调用, 为了达成这个目的或者是其他的目的, 每一个对象同时都包含着一个指向其自身类型对象的指针。

最大的问题仍然是: 什么时候去使用那两个(x)呢? 我们最先来引入一些概念。不存在任何人能够"拥有"一个对象 , 能拥有的是到一个对象的引用。一个对象的引用计数被定义成拥有引用的数量。引用的拥有者负有责任去调用某个东西 , 当引用不再被需要的时候。引用的拥有关系是能够被传递的。存在三种方式来处置所拥有的引用: 传递 、存储 、调用那个()。一旦忘记处置所拥有的引用就会致使内存泄漏。

还能够借用一个对象的引用, 所借用的引用不会调用, 借用者应当务必保证不会持有对象超出拥有者借出的时长, 在拥有者处置对象后去使用借用到的引用存有风险, 是应当彻底避免的。

相对于引用而言, 借用所具备的优点在于, 你不用去操心整条路径上代码的引用情况, 也就是说, 借助借用你不用担心会存在内存泄漏的风险。而借用存在的缺点是, 某些在看起来正确的代码上的借用, 可能会在拥有者处置完之后还使用对象。

能将借用转变为拥有引用, 借助调用达成。这不会对已然借出的拥有者的状态造成影响。这会构建出一个全新的拥有引用, 同时赋予全然的拥有者责任(全新的拥有者务必妥善地处置该引用, 正如先前的拥有者那样)。

1.10.2. 拥有规则

当有一个对象引用被传递到函数内部, 之后又从中传递出来时, 这个函数的接口应当明确规定, 拥有关系的该项传递之中, 是否涵盖了引用这一情况。

大多数函数返回一个对象的引用, 且传递引用拥有关系, 通常, 各类创建对象的函数, 诸如那样的, 会把拥有关系传递给接收者, 即便对象并非真正全新的, 你依旧能够获取对象的新引用, 有一个实例是,它维护了一个流行值的缓存, 并且能够返回已缓存项目的新引用。

有许多针对另一个对象去提取对象的函数, 同样会进行引用关系的传递。比如, 此处情形不那么明晰。存有一些不太常被使用的例程属于例外。这些例程, 它们全都是返回从元组、列表、字典里所借用的引用。

哪怕函数可能会返回创建的对象, 它也会返回借用的引用, 这可能是因为一个拥有的引用对象存储在sys.里。

当你把一个对象引用传递给另一个函数的时候, 一般情况下函数是被借用的状态。要是有存储的需求, 那就使用 从而转变成为独立的拥有者。对于这个规则来说, 存在两个重要的例外情况: 和。这些函数会接纳传递过来的引用关系, 哪怕出现失败的状况!(需要留意的是 以及和它同类的不会接受引用关系, 它们属于"正常的"范畴)。

当一个C函数展开被调用的行为时, 会从调用的一方传来有关于参数的借用那种引用, 调用的那一方拥有对象的引用, 所以被这样借用的引用的生命周期能够去保证一直坚持抵达到函数返回, 只要当被借用的这种引用需要执行储存举措或者进行传递操作时, 就必然得转换成为真正拥有的引用, 借助于调用。

在调用源自C函数返回的对象引用之际, 所采用的必须得是具备拥有性质的引用, 而这种拥有关系是从该函数传导至调用者处的。

1.10.3. 危险的薄冰

具备少数情形之下, 遭到借用的引用看似无损于任何事情, 然而却极有可能引发问题。一般而言这是源于解释器施以的隐式调用, 并且有可能致使引用的拥有者对这个引用进行处置。

首先, 要格外留意的情形有, 用到了一个毫无关联的对象, 并且, 该对象的引用是从一个列表的元素那里借用来的。举个例子:

复制代码
void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* BUG! */
}

这个函数先是借助一个引用list, 接着将list替换成值0, 最后把借用的引用打印出来。看似无害对吧, 可事实并非如此。

我们跟着、控制流进去。 这个列表、有着对它所有条目的引用, 所以当条目 1 被替换时, 它得丢弃原本的条目 1。 此刻我们假设原本的条目 1 是某个用户自定义类的实例, 并且假设该类定义了 () 方法。 要是该类实例的引用计数为 1, 丢弃它就会调用其 () 参数。 在内部, () 会调用被替换条目的 , 这会唤起被替换那个条目的对应的 函数。在处于撤销分配的那个期间之内, 会发生调用的情况, 这个调用所涉及的它, 是被映射到专门用于类实例的那个有着特定括号内容的方法里的(这可以去参见PEP 442)。而以上所描述的整个这个过程, 是在有着特定括号内容的调用的内部进行同步发生的。

因它是以 编写而成的, 所以 () 方法能够执行任意 代码。它有没有可能造成 bug() 里对于 item 的引用变成无效呢? 那是肯定可以的!假设传入 bug() 的列表能够被 () 方法所访问, 它就能执行一条语句达成 del list 的效果, 假设这是针对该对象的最后一次引用, 它便会释放与之相关联的内存, 进而让 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);
}

这是个真实的事儿, 一个曾包含此问题变化形式的较旧版本, 有人在C语言调试器里耗费大量时间, 才搞清楚为啥他的()方法会失败......

有关借入引用的相关问题的第二种情形是关涉线程的变体, 寻常状况下, 解释器里的多个线程彼此之间不会产生相互影响, 缘由在于存在一个对整个对象空间予以保护的机制, 然而, 存在一种可能性, 即运用宏来临时解除这个锁, 并且运用其来再度获取它, 这在阻塞型的 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的指针时, 比如这个指针是来自于某个函数调用, 或者是来自于一个会引发异常情况的特别规定时。

等宏当中, 不会对NULL指针进行检测的是和, 不过, 会检测那类指针的则是和它们相应的变种哟。

针对特定对象类型进行检测的宏 (()), 不会对 NULL 指针予以检测, 同样地, 存在大量代码, 会去连续调用这些宏, 以此来测试一个对象是不是几种不同预期类型当中的某一种, 这就会生成冗余的测试, 不存在带有 NULL 检测的变体。

C函数调用机制拥有这样一种保障, 即传递给C函数的那个参数列表, 在本示例里是args, 绝对不会让其呈现为NULL的状貌, 事实上呢, 该机制会做出保证, 它必然一直维持为一个元组的形态。

不论在何时, 把 NULL 指针透露给用户, 这都会是一种严重的失误。

1.11. 在C++中编写扩展

相关推荐
Yanjun2i1 小时前
Agent学习记录五:Pydantic验证
人工智能·python·学习
武雄(小星Ai)1 小时前
飞书API上传26MB文件偶发失败:错误码9499与空响应体排坑实录
后端·api·排坑实录
月光船幽幽2 小时前
跨范式映射的稳定接口设计
人工智能·python·算法
杜大哥2 小时前
python程序:如何查看电脑【电池电量的剩余百分比】 和 【是否插入连接着充电器】?
开发语言·python
wuyk5552 小时前
Python网络爬虫入门到实战 第01章:爬虫到底是什么?原理、流程、合法性、风险全解析(零基础必看)
开发语言·爬虫·python
信誓旦旦的程序猿2 小时前
【量化系统从零构建 #04】存储设计:选型·建库·交易日历
java·人工智能·python·股票数据api·股票数据·股票数据api接口·股票api数据接口
萧鼎3 小时前
Python 高性能Web框架神器 FastAPI:自动生成API文、基于Pydant、异步请求处理全搞定
前端·python·fastapi
2601_962299883 小时前
Linux执行Python脚本方法
linux·python·脚本·解释器·shebang