使用 C 或 C++ 开发 Python库(01)

news2024/10/22 2:39:06

文章目录

  • 一、说明
  • 二、一个简单的例子
  • 三、间奏:错误和异常
  • 四、回到例子
  • 五、模块的方法表和初始化函数
  • 六、编译和链接
  • 七、从 C 调用 Python 函数
  • 八、提取扩展函数中的参数
  • 九、扩展函数的关键字参数
  • 十、构建任意值
  • 十一、引用计数
    • 11.1 Python 中的引用计数
    • 11.2.所有权规则
    • 11.3.Thin Ice
    • 11.4.空指针
  • 十二、用 C++ 编写扩展
  • 十三、为扩展模块提供 C API

一、说明

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

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

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

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

二、一个简单的例子

让我们创建一个名为spam(Monty Python 粉丝最喜欢的食物……)的扩展模块,假设我们要为 C 库函数system() [ 1 ]创建一个 Python 接口。此函数以空字符结尾的字符串作为参数并返回一个整数。我们希望此函数可以从 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用于指示Py_ssize_t在某些 API 中应使用 而不是int。自 Python 3.13 以来,它不再是必需的,但我们将其保留在此处以实现向后兼容。有关此宏的描述,请参阅字符串和缓冲区。

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

我们接下来要添加到模块文件中的是在 Python 表达式被求值时被调用的 C 函数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 函数始终有两个参数,通常命名为self 和args。“ls -l”

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

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

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

三、间奏:错误和异常

整个 Python 解释器中的一个重要约定是:当函数失败时,它应该设置异常条件并返回错误值(通常-1为NULL指针)。异常信息存储在解释器线程状态的三个成员中。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本身应该返回一个错误值(通常是 或)。它不应该调用其中一个函数 — 其中一个已经被g调用过。然后f的调用者也应该向其调用者返回错误指示,同样不调用,依此类推 — 最详细的错误原因已由最先检测到它的函数报告。一旦错误到达 Python 解释器的主循环,就会中止当前正在执行的 Python 代码并尝试查找 Python 程序员指定的异常处理程序。NULL-1PyErr_PyErr_

(在某些情况下,模块实际上可以通过调用另一个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.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);
}

四、回到例子

回到我们的示例函数,你现在应该能够理解这个语句:

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 对象的 C 名称None。它是一个真正的 Python 对象,而不是NULL指针,正如我们所见,在大多数情况下,它意味着“错误”。

五、模块的方法表和初始化函数

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

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

注意第三个条目 ( METH_VARARGS)。这是一个标志,告诉解释器要用于 C 函数的调用约定。它通常应该始终是METH_VARARGS或;值为表示使用了的过时变体。METH_VARARGS | METH_KEYWORDS0PyArg_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模块文件中定义的唯一非 item:

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

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

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

嵌入 Python 时,PyInit_spam()除非表中有条目,否则不会自动调用该函数PyImport_Inittab。要将模块添加到初始化表,请使用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在一个进程中从多个解释器中删除条目或将已编译模块导入到多个解释器中(或fork()在中间没有插入的情况下跟进exec())可能会给某些扩展模块带来问题。扩展模块作者在初始化内部数据结构时应谨慎行事。
Python 源代码分发版中包含一个更实质性的示例模块,即Modules/xxmodule.c。此文件可用作模板或仅作为示例阅读。

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

六、编译和链接

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

如果你不能使用动态加载,或者你想让你的模块成为 Python 解释器的永久组成部分,那么你必须更改配置设置并重建解释器。幸运的是,这在 Unix 上非常简单:只需将文件(spammodule.c例如)放在Modules/解压后的源代码分发目录中,在文件中添加一行 Modules/Setup.local描述你的文件:

spam spammodule.o

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

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

spam spammodule.o -lX11

七、从 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()在调用后立即被赋值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);

八、提取扩展函数中的参数

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

九、扩展函数的关键字参数

该PyArg_ParseTupleAndKeywords()函数声明如下:

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

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

笔记 使用关键字参数时无法解析嵌套元组!传入的关键字参数如果在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);
}

十、构建任意值

此函数与 相对应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))

十一、引用计数

在 C 或 C++ 等语言中,程序员负责动态分配和释放堆上的内存。在 C 中,这是使用函数 malloc()和完成的free()。在 C++ 中,运算符new和 的 delete含义基本相同,我们将以下讨论限制在 C 的情况下。

每个分配给的内存块malloc()最终都应通过一次调用返回到可用内存池中。在正确的时间free()调用非常重要。如果忘记了块的地址但未调用它,则它占用的内存在程序终止之前不能重新使用。这称为内存泄漏。另一方面,如果程序调用一个块然后继续使用该块,则会与通过另一个调用重新使用该块产生冲突。这称为使用释放的内存。它具有与引用未初始化数据相同的不良后果 - 核心转储、错误结果、神秘崩溃。free()free()free()malloc()

内存泄漏的常见原因是代码中的异常路径。例如,一个函数可能会分配一个内存块,进行一些计算,然后再次释放该块。现在,函数要求的变化可能会为计算添加一个测试,该测试可检测错误情况并可能过早从函数返回。在过早退出时很容易忘记释放分配的内存块,尤其是当它稍后添加到代码中时。一旦引入此类泄漏,通常会很长时间都无法检测到:错误退出仅在所有调用的一小部分中发生,并且大多数现代机器都有足够的虚拟内存,因此泄漏只会在频繁使用泄漏函数的长时间运行的进程中变得明显。因此,通过制定编码约定或策略来最大限度地减少此类错误,以防止发生泄漏非常重要。

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

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

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

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

11.1 Python 中的引用计数

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

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

也可以借用 [ 2 ]一个对象的引用。引用的借用者不应调用Py_DECREF()。借用者持有该对象的时间不得超过借用者持有的时间。在所有者释放引用后使用借用的引用可能会占用已释放的内存,应完全避免[ 3 ]。

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

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

11.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 函数返回的对象引用必须是拥有的引用——所有权从函数转移到其调用者。

11.3.Thin Ice

在某些情况下,看似无害的借用引用可能会导致问题。这些都与解释器的隐式调用有关,这可能导致引用的所有者将其释放。

首先要了解的也是最重要的情况是,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中的引用无效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! */
}

11.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 用户手中是一个严重的错误。

十二、用 C++ 编写扩展

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

十三、为扩展模块提供 C API

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

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

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

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

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

无论选择哪种方法,正确命名 Capsule 都很重要。该函数PyCapsule_New()采用名称参数 ( 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节中模块 的修改。函数不直接调用 C 库函数,而是调用一个函数 ,该函数实际上当然会执行更复杂的操作(例如向每个命令添加“垃圾邮件”)。此函数 还导出到其他扩展模块。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 (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 的实现(文件 Include/pycapsule.h和Objects/pycapsule.cPython 源代码分发)中进行了描述。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2219349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PHP爬虫API:获取商品详情的新利器

为什么选择PHP爬虫API 灵活的数据处理&#xff1a;PHP强大的数据处理能力&#xff0c;使得从API获取的数据可以被快速地处理和分析。丰富的库支持&#xff1a;PHP拥有如cURL、Guzzle等库&#xff0c;这些库简化了HTTP请求的发送和响应的接收。易于集成&#xff1a;PHP作为服务…

无人机搭载激光雷达在地形测绘中的多元应用

一、高精度地形测量 无人机激光雷达能够发射激光脉冲并接收其回波&#xff0c;通过精确计算激光脉冲的往返时间来确定目标物的距离。这一特性使得无人机激光雷达在地形测绘中能够实现高精度的三维地形测量。通过快速获取大量地形数据&#xff0c;可以生成高精度的数字高程模型…

pytorh学习笔记——cifar10(一)生成数据

CIFAR&#xff08;Canadian Institute For Advanced Research&#xff09;是一个用于图像识别研究的数据集。CIFAR数据集包含多个子数据集&#xff0c;最常用的是CIFAR-10和CIFAR-100。 CIFAR-10数据集包含60000张32x32彩色图像&#xff0c;分为10个类别&#xff0c;每…

SpringCloud无介绍快使用,单机Eureka服务注册中心cloud-eureka-server7001搭建(十)

TOC 问题背景 从零开始学springcloud微服务项目 注意事项&#xff1a; 约定 > 配置 > 编码IDEA版本2021.1这个项目&#xff0c;我分了很多篇章&#xff0c;每篇文章一个操作步骤&#xff0c;目的是显得更简单明了controller调service&#xff0c;service调dao项目源码以及…

Python学习的自我理解和想法(17)

学的是b站的课程&#xff08;千锋教育&#xff09;&#xff0c;跟老师写程序&#xff0c;不是自创的代码&#xff01; 今天是学Python的第17天&#xff0c;学的内容是面向对象设计。开学了&#xff0c;时间不多&#xff0c;写得不多&#xff0c;见谅。 目录 1.面向对象入门 …

基于PHP+MySQL+Vue的网上订餐系统

摘要 本文介绍了一个基于PHPMySQLVue技术的网上订餐系统。该系统旨在为用户提供便捷的在线订餐服务&#xff0c;同时提高餐厅的运营效率。系统后端采用PHP语言开发&#xff0c;利用MySQL数据库进行数据存储与管理&#xff0c;实现了用户注册登录、菜品浏览、购物车管理、订单提…

es kibana .logstash离线集群安装

es离线集群安装 下载对应的版本一般看你客户端引用的是什么版本我这里下载的是7.6.2 官方下载地址&#xff1a;https://www.elastic.co/cn/downloads/elasticsearch 源码安装-环境准备&#xff1a;在etc/hosts文件添加3台主机 node-001 192.168.1.81 node-002 19…

图像中的数值计算

目录 图像读取与形状图像数据展示图像数据操作超出范围的像素值处理 图像读取与形状 使用cv2.imread函数读取图像文件。图像的形状通过shape属性获取&#xff0c;格式为(高度, 宽度, 颜色通道数)。 import cv2img1 cv2.imread(bg.jpg) img2 cv2.imread(fish.jpg)print(img1…

微信小程序:miniprogram-ci自动打包工具使用介绍以及支持配置环境变量、jekins打包、taro、uni-app三方工具

微信小程序&#xff1a;miniprogram-ci自动打包工具使用介绍以及支持配置环境变量、jekins打包、taro、uni-app三方工具 背景介绍 一直都是本地电脑运行微信开发者工具打包上传。多项目中新老版本对node版本要求不一致&#xff0c;老是切来切去。而且同一个人开发上传需要打包…

求最大公约数(c语言)

先看题&#x1f447; 我这里介绍的方法&#xff1a;辗转相除法&#xff1a; 最大公约数&#xff1a; 最大公约数是指同时能整除俩个或更多整数的最大正整数。 欧几里得算法就是求最大公约数的算法 求最大公约数涉及到一个数学原理的转换: 俩个数的最大公约数等于其中一个数和…

关于我、重生到500年前凭借C语言改变世界科技vlog.7——数组函数实践

文章目录 扫雷游戏专题1.扫雷游戏的设计分析1.1 棋盘1.2 文件 2.扫雷游戏的实现3.扫雷游戏的扩展 希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力&#xff01; 扫雷游戏专题 掌握了前面的数组与函数的知识&#xff0c;我们可以制作一款大多电脑上都有的简易…

公交信息在线查询系统|基于java和小程序的公交信息在线查询系统小程序设计与实现(源码+数据库+文档)

公交信息在线查询系统小程序 目录 基于java和小程序的公交信息在线查询系统小程序设计与实现 一、前言 二、系统功能设计 三、系统实现 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂…

解锁文本数据可视化的无限可能:Wordcloud库全解析

文章目录 **&#x1f31f;解锁文本数据可视化的无限可能&#xff1a;Wordcloud库全解析&#x1f510;**1. **背景介绍**2. **Wordcloud库是什么&#xff1f;**3. **如何安装Wordcloud库&#xff1f;**4. **Wordcloud库的基本函数使用方法**5. **实际应用场景**6. **常见问题及解…

VUE 仿神州租车-开放平台

项目背景&#xff1a; 神州租车是一家提供汽车租赁服务的公司&#xff0c;其API开放平台为开发者提供了访问神州租车相关服务和数据的接口。用VUE技术来仿照其开发平台。 成果展示&#xff1a; 首页&#xff1a; API文档&#xff1a; 关于我们&#xff1a;

MyBatis实践:提高持久层数据处理效率

文章目录 1 Mybatis简介1.1 简介1.2 持久层框架对比 2 快速入门2.1 准备数据库2.2 项目搭建2.3 依赖导入2.4 准备MyBatis配置文件2.5 实体类准备2.6 准备Mapper接口和MapperXML文件2.7 运行和测试 3. 核心配置文件4. MyBatis进阶使用4.0 以包为单位&#xff0c;引入所有的映射文…

算法Day-4

24. 两两交换链表中的节点 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,…

Windows10去掉隐藏文件仍找不到hosts文件的解决办法

正常情况下hosts文件在目录C:\Windows\System32\drivers\etc中&#xff0c;最近新装的Windows10系统发现该目录下没有hosts文件。 执行如下命令hosts文件出现&#xff1a; 执行 for /f %P in (dir %windir%\WinSxS\hosts /b /s) do copy %P %windir%\System32\drivers\etc &am…

ubuntu 20.04 网卡启用后,只有ipv6 没有 ipv4 无法上网

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

JS | JS之深入理解客户区尺寸client系列属性

目录 一、客户区大小 clientHeight clientWidth clientLeft clientTop 二、页面大小 三、注意事项 关于元素尺寸&#xff0c;一般地&#xff0c;有偏移大小offset、客户区大小client和滚动大小scroll。前文已经介绍过偏移属性&#xff0c;后文将介绍scroll滚动大小&…

责任链模式下,解决开闭原则问题实践

前言 在现代软件工程中&#xff0c;设计模式是解决常见问题的有效工具之一。它们吸收了前人的经验&#xff0c;不仅帮助开发者编写更清晰、更可维护的代码&#xff0c;还能促进团队之间的沟通和协作。责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;作为一…