带可变参数的Python扩展模块

14 投票
2 回答
6980 浏览
提问于 2025-04-17 05:36

我正在尝试弄清楚在C扩展模块中,如何让一个函数接受很多(可能还挺多的)参数。

我读到关于PyArg_ParseTuple的内容,似乎你必须知道要接受多少个参数,有些是必需的,有些是可选的,但每个参数都得有自己的变量。我原本希望PyArg_UnpackTuple能处理这个问题,但当我尝试用错的方法时,它似乎只给我带来了总线错误。

举个例子,看看下面这段Python代码,可能想把它做成一个扩展模块(用C语言)。

def hypot(*vals):
    if len(vals) !=1 :
        return math.sqrt(sum((v ** 2 for v in vals)))
    else: 
        return math.sqrt(sum((v ** 2 for v in vals[0])))

这个函数可以用任意数量的参数调用,像这样:hypot(3,4,5)hypot([3,4,5]),还有hypot(*[3,4,5]),这三种调用方式都会得到相同的结果。

我C语言函数的开头看起来是这样的:

static PyObject *hypot_tb(PyObject *self, PyObject *args) {
// lots of code
// PyArg_ParseTuple or PyArg_UnpackTuple
}

非常感谢yasar11732。这里给下一个人提供一个完整的工作扩展模块(_toolboxmodule.c),它可以接受任意数量的整数参数,并返回一个由这些参数组成的列表(名字起得不太好)。这是个玩具示例,但说明了需要做的事情。

#include <Python.h>

int ParseArguments(long arr[],Py_ssize_t size, PyObject *args) {
    /* Get arbitrary number of positive numbers from Py_Tuple */
    Py_ssize_t i;
    PyObject *temp_p, *temp_p2;

    for (i=0;i<size;i++) {
        temp_p = PyTuple_GetItem(args,i);
        if(temp_p == NULL) {return NULL;}

        /* Check if temp_p is numeric */
        if (PyNumber_Check(temp_p) != 1) {
            PyErr_SetString(PyExc_TypeError,"Non-numeric argument.");
            return NULL;
        }

        /* Convert number to python long and than C unsigned long */
        temp_p2 = PyNumber_Long(temp_p);
        arr[i] = PyLong_AsUnsignedLong(temp_p2);
        Py_DECREF(temp_p2);
    }
    return 1;
}

static PyObject *hypot_tb(PyObject *self, PyObject *args)
{
    Py_ssize_t TupleSize = PyTuple_Size(args);
    long *nums = malloc(TupleSize * sizeof(unsigned long));
    PyObject *list_out;
    int i;

    if(!TupleSize) {
        if(!PyErr_Occurred()) 
            PyErr_SetString(PyExc_TypeError,"You must supply at least one argument.");
        return NULL;
    }
    if (!(ParseArguments(nums, TupleSize, args)) { 
        free(nums);
        return NULL;
    }

    list_out = PyList_New(TupleSize);
    for(i=0;i<TupleSize;i++)
        PyList_SET_ITEM(list_out, i, PyInt_FromLong(nums[i]));
    free(nums);
    return (PyObject *)list_out;
}

static PyMethodDef toolbox_methods[] = {
   { "hypot", (PyCFunction)hypot_tb, METH_VARARGS,
     "Add docs here\n"},
    // NULL terminate Python looking at the object
     { NULL, NULL, 0, NULL }
};

PyMODINIT_FUNC init_toolbox(void) {
    Py_InitModule3("_toolbox", toolbox_methods,
                     "toolbox module");
}

在Python中,它是:

>>> import _toolbox
>>> _toolbox.hypot(*range(4, 10))
[4, 5, 6, 7, 8, 9]

2 个回答

0

谢谢@yasar,模板非常感谢你。而接下来的这个改编可能有点过于简单了——不过简单总是好的,对吧?

ctst.c:

/*
 *  https://realpython.com/build-python-c-extension-module
 *
 *  gcc -O2 -fPIC -c ctst.c $(python3-config --includes)
 *  gcc -shared ctst.o -o ctst.so
 */

#include <Python.h>

static PyObject *
cargs(PyObject *self, PyObject *args)
{
    Py_ssize_t n_args = PyTuple_Size(args);
    PyObject * lst = PyList_New(0);

    for (int i = 0; i < n_args; i++) {
        PyObject * item = PyTuple_GetItem(args, i);
        // play with items one at a time ...
        // yes, malloc is needed to play with all
        // items outside this loop
        PyList_Append(lst, item);
    }

    return lst;
}

static PyMethodDef MyCtstMethods[] = {
    {"cargs", fcrgs, METH_VARARGS,
    "toss a bunch of Python vars into a C function, return the list (maybe) modified"},
    {NULL, NULL, 0, NULL}
};


static struct PyModuleDef ctst = {
    PyModuleDef_HEAD_INIT, "ctst", "testing C calls", -1, MyCtstMethods
};

PyMODINIT_FUNC
PyInit_ctst(void)
{
    return PyModule_Create(&ctst);
}

tst.py:

import ctst

print('test0 (null):', ctst.cargs())
print('test1 (ints):', ctst.cargs(1, 2, 3))
print('test2 (mix) :', ctst.cargs(1, 2, 3, 'hi'))
print('test3 (flt) :', ctst.cargs(0.1234))
print('test4 (lst) :', ctst.cargs(['a', 'b', 'c']))
11

我之前用过类似的东西。可能这段代码写得不好,因为我不是很有经验的C语言程序员,但对我来说是有效的。这里的关键是,*args其实就是一个Python的元组,你可以对它做任何在Python元组上能做的事情。你可以查看一下这个链接了解更多信息:http://docs.python.org/c-api/tuple.html

int
ParseArguments(unsigned long arr[],Py_ssize_t size, PyObject *args) {
    /* Get arbitrary number of positive numbers from Py_Tuple */
    Py_ssize_t i;
    PyObject *temp_p, *temp_p2;


    for (i=0;i<size;i++) {
        temp_p = PyTuple_GetItem(args,i);
        if(temp_p == NULL) {return NULL;}

        /* Check if temp_p is numeric */
        if (PyNumber_Check(temp_p) != 1) {
            PyErr_SetString(PyExc_TypeError,"Non-numeric argument.");
            return NULL;
        }

        /* Convert number to python long and than C unsigned long */
        temp_p2 = PyNumber_Long(temp_p);
        arr[i] = PyLong_AsUnsignedLong(temp_p2);
        Py_DECREF(temp_p2);
        if (arr[i] == 0) {
            PyErr_SetString(PyExc_ValueError,"Zero doesn't allowed as argument.");
            return NULL;
        }
        if (PyErr_Occurred()) {return NULL; }
    }

    return 1;
}

我调用这个函数的方式是这样的:

static PyObject *
function_name_was_here(PyObject *self, PyObject *args)
{
    Py_ssize_t TupleSize = PyTuple_Size(args);
    Py_ssize_t i;
    struct bigcouples *temp = malloc(sizeof(struct bigcouples));
    unsigned long current;

    if(!TupleSize) {
        if(!PyErr_Occurred()) 
            PyErr_SetString(PyExc_TypeError,"You must supply at least one argument.");
        free(temp);
        return NULL;
    }

    unsigned long *nums = malloc(TupleSize * sizeof(unsigned long));

    if(!ParseArguments(nums, TupleSize, args)){
        /* Make a cleanup and than return null*/
        return null;
    }

撰写回答