Pybind11 揭秘。 Ch 4.1:仅位置参数

Pybind11 揭秘。 Ch 4.1:仅位置参数

Python 不支持 C++ 中的函数重载(具有相同函数名称的不同函数签名)。尽管如此,Python 确实提供了一组少数语法,包括默认值和 kwargs,以允许根据参数改变函数的行为。

本章将探讨在 Python 扩展中处理 Python 风格的函数重载的不同方法,以及如何为它们编写 C 扩展。

Ch 4.1 仅位置参数

根据扩展函数是否使用任何位置参数,它将被调用 METH_VARARGS 或者 METH_VARARGS | METH_KEYWORDS 公约。 详情可查 这里 .

在本章中,我们将关注仅具有位置参数的函数,即 METH_VARARGS 惯例。

来看看我们的 xx模块.c

静态 PyMethodDef xx_methods[] = {
{“roj”,xx_roj,METH_VARARGS,
PyDoc_STR("roj(a,b) -> 无")},
{“foo”,xx_foo,METH_VARARGS,
xx_foo_doc},
{“新”,xx_new,METH_VARARGS,
PyDoc_STR("new() -> new Xx object")},
{“错误”,xx_bug,METH_VARARGS,
PyDoc_STR("bug(o) -> 无")},
{NULL, NULL} /* 哨兵 */
};

这个列表定义了所有模块的功能,让我们通过扩展列表来添加一个新的。就我而言,我称之为 xx_arg 骨架实现如下。

静态 PyObject *
xx_arg(PyObject *self, PyObject *args)
{
Py_IncRef(Py_None);
返回 Py_None;
}

请注意,从 C 扩展函数返回的任何内容都将被一个变量捕获,并且当捕获变量超出范围时,其引用计数将减少。所以我们需要系统地 将返回值的引用计数增加 1 以避免负引用计数。这同样适用于 None,一个定义的 Python 对象 这里 .

除了返回 None,我们还可以返回 参数 看看它是什么。

静态 PyObject *
xx_arg(PyObject *self, PyObject *args)
{
Py_IncRef(args);
返回参数;
}

然后在 Python 中:

>>> 进口 xx
>>> 打印(xx.arg(9.,2.5))
(9.0, 2.5)

好的。 参数 只是输入参数的元组,基本上 *参数 .

或者我们也可以检查 a 的类型 PyObject , 和 Py*_检查 方法,例如 PyTuple_Check

在 C 扩展中检查变量类型的另一种方法是利用[ PyTypeObject](https://github.com/python/cpython/blob/6fd4c8ec7740523bb81191c013118d9d6959bc9d/Include/object.h#L134) .

printf(“类型:%s\n”, Py_TYPE(args)->tp_name);

甚至更好,与 PyObject_Print ,您可以打印对象的字符串表示形式。

一旦我们确定变量是 PyTuple ,我们可以通过 PyTuple_Size .我们也可以得到它的第 i 个孩子 PyTuple_GET_ITEM

将所有这些放在一起,我们可以得到第二个版本,如下所示:

静态 PyObject *
xx_arg(PyObject *self, PyObject *args)
{

断言(PyTuple_Check(args)); Py_ssize_t 计数 = PyTuple_Size(args);
for (int i = 0; i < 计数; i++)
{
printf("类型: %s\n", Py_TYPE(element)->tp_name);
} Py_IncRef(Py_None);
返回 Py_None;
}

看起来很棒。但这很长;如果我可以使用 C/C++ 类型函数定义一个函数,而不是手动将元组中的每个 PyObject 转换为其对应的类型,那就太好了,这正是 Pybind11 所做的,使用复杂的 C++ 模板内容。

让我们做一个最小的重新实现以使事情变得清晰。

我们需要做的第一件事就是改变 xx模块.c xx模块.cc 并使用 g++它 代替 海合会 作为编译器。

然后我们需要以 C/C++ 风格定义实际的函数体。

int func(int a,float b)
{
返回 a + b + 1;
}

最后,我们需要完成将 PyObject* 元素从元组转换为其对应的 C 类型的困难部分。

对于每种类型,让我们定义一个 过程 转换函数 PyObject* 到所需的类型。这是我的演示实现。

模板
参数处理(PyObject *op); 模板<>
整数进程(PyObject *操作)
{
int v = _PyLong_AsInt(op);
printf("处理 int: %d\n", v);
返回 v;
} 模板<>
肺突(PyObject *操作)
{
长 v = PyLong_AsLong(op);
printf("进程长:%ld\n", v);
返回 v;
} 模板<>
浮动过程(PyObject *操作)
{
浮动 v = PyFloat_AsDouble(op);
printf("处理浮点数:%lf", v);
返回 2.;
}

然后让我们将每个处理过的 PyObject 放入一个元组并将其传递给目标 F 功能。提供 C++ 17 标准::应用 对于这项工作非常方便。这是完整的代码。

模板<typename Return, typename... Args, std::size_t... Idx>
Return _expand(Return (*f)(Args...), PyObject tuple, std::index_sequence<Idx...> )
{
返回 std::apply(f, std::make_tuple(进程(PyTuple_GetItem(tuple, Idx))...));
} 模板<typename Return, typename... Args>
返回扩展(返回(
f)(Args ...),PyObject *元组)
{
return _expand(f, tuple, std::make_index_sequence<sizeof...(Args)> ());
}

乍一看似乎有点复杂,但实际上很简单。

std::make_index_sequence<sizeof…(Args)> () 返回一个由 0、1、2、...、N 组成的序列,其中 N 是目标中的参数数量 F 功能。

std::make_tuple(进程<Args>(PyTuple_GetItem(tuple, Idx))…) 意味着我们正在创建一个已处理元素的元组。 ... 的一般经验法则是在参数包中的每个值的点之前重复该语句。展开形式与以下类似。

std::make_tuple(
过程(PyTuple_GetItem(tuple, 0)),
过程(PyTuple_GetItem(tuple, 1)),
过程(PyTuple_GetItem(tuple, 2)),
...
过程(PyTuple_GetItem(元组,N)))

标准::应用 扩展此元组并将其应用于 F , IE

F(
过程(PyTuple_GetItem(tuple, 0)),
过程(PyTuple_GetItem(tuple, 1)),
过程(PyTuple_GetItem(tuple, 2)),
...
过程(PyTuple_GetItem(元组,N)))

很酷。不是吗?

但是我们遗漏了一些东西……类型检查怎么样?如果传递的参数与所需的 C 类型不兼容怎么办?

让我们将类型检查作为练习留给您。使用上面的示例,您应该能够编写该代码。

(在完成练习之前不要翻页。)

这是一个参考实现。例如,我相信您可以获得更好的版本,并提供更有意义的错误报告(请在评论中发布您的版本!)。

模板
布尔检查(PyObject
{
返回假;
} 模板<>
布尔检查(PyObject 操作)
{
返回 PyLong_CheckExact(op);
} 模板<>
布尔检查(PyObject
开启){
返回 PyFloat_CheckExact(op);
} 模板<typename Return, typename... Args>
返回扩展(返回(
f)(Args ...),PyObject 元组)
{
return _expand(f, tuple, std::make_index_sequence<sizeof...(Args)> ());
} 模板<typename Return, typename... Args, std::size_t... Idx>
Return _expand(Return (
f)(Args...), PyObject tuple, std::index_sequence<Idx...> )
{
**自动检查 = std::initializer_list {检查 (PyTuple_GetItem(tuple, Idx))...};
for (int i = 0; i < checks.size(); i++)
{
if (!
(checks.begin() + i))
{
printf("发现 %d arg\n 有问题", i);
返回返回();
}
}**
返回 std::apply(f, std::make_tuple(进程(PyTuple_GetItem(tuple, Idx))...));
}

好的。这就是本章的内容。现在通过调用尝试一下 展开(YOUR_TARGET_FUNCTION,ARGS_TUPLE) 在您新创建的函数中。

您可能已经注意到,这种调用转换需要一个中间 Python 元组,而它可能只是使用了一个低级 C 样式的数组。 https://bugs.python.org/issue29259 提出了一种称为快速调用的新转换。我们将很快介绍这一点。

第 4.2 章:待定

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/15630/37370510

posted @ 2022-09-05 10:38  哈哈哈来了啊啊啊  阅读(246)  评论(0)    收藏  举报