如何允许None或特定类型作为Python C Extension函数的参数?

时间:2017-02-19 21:52:59

标签: python python-2.7 python-c-api python-c-extension

说我有一个如下设计的功能:

static int foo(PyObject *self, PyObject *args) {
    char *a = "";
    char *b = "";
    int c = 0;

    if (!PyArg_ParseTuple(args, "ss|i", &a, &b, &c) {
        return NULL;
    }

    printf("c is %i\n", c);

    //some_function_requiring_int_data_type(c);
}

我希望用户能够提交intNone作为c arg的值,但上述代码不允许这样做:

>>>from spam import foo
>>>foo('a', 'b')
c is 0
>>>foo('a', 'b', 100)
c is 100
>>>foo('a', 'b', None)
TypeError: an integer is required

目前,为了启用此行为,我有一堆丑陋的代码,如下所示:

static int foo(PyObject *self, PyObject *args) {
    char *a = "";
    char *b = "";
    PyObject *c = NULL; // Note how I use PyObject *
    int c_int = 0; // Note how I have an accompanying int

    if (!PyArg_ParseTuple(args, "ss|O", &a, &b, &c) {
        return NULL;
    }

    // Ugly code starts here
    if (c) {
        if (c != Py_None) {
            if (!PyInt_Check(c)) {
                PyExc_SetString(PyExc_TypeError, "c must be int or None");
                return;
            }
            c_int = PyInt_AsSsize_t(c);
        }
    }

    printf("c_int is %i\n", c_int);

    //some_function_requiring_int_data_type(c_int);
}

及其用途:

>>>from spam import foo
>>>foo('a', 'b')
c is 0
>>>foo('a', 'b', 100)
c is 100
>>>foo('a', 'b', None)
c is 0

2 个答案:

答案 0 :(得分:2)

我的第一个建议是使用仅关键字参数。这样做的主要优点是避免传递None占位符值,因为您永远不必“填写”(比方说)一个未指定的第三个位置参数,这样您就可以指定第四个。它基本上将Python界面改为“与你的意思相匹配”。

static PyObject* int_from_kw(PyObject* self, PyObject* args, PyObject* kwargs) {
    char *a, *b;
    Py_ssize_t c = 0; // default value

    char* kwarg_names[] = {"a","b","c",NULL};

    // optional check to ensure c is passed only as a keyword argument - not needed with Python 3
    if (PyTuple_Size(args)>2) {
        PyErr_SetString(PyExc_TypeError,"Only two positional arguments allowed");
        return NULL;
    }

    if (!PyArg_ParseTupleAndKeywords(args,kwargs,"ss|i",kwarg_names,&a,&b,&c)) {
        return NULL;
    }
    printf("c_int is %li\n", c);
    return PyLong_FromSsize_t(c);
}

(在Python 3中,你可以删除长度检查并使用"ss|$i"来指定$之后只有关键字的参数,这有点好一点。您需要将函数类型指定为METH_VARARGS|METH_KEYWORDS

然后您可以从Python中调用它

int_from_kw("something","something else") # default c
int_from_kw("something","something else",c=5)
int_from_kw(a="something",b="something else",c=5) # etc

但不是

int_from_kw("something","something else",c="not an int")
int_from_kw("something","something else",5)

缺点是该方法并不总是有效 - 有时您需要该函数符合第三方库强制执行的固定接口。

我的第二个建议是使用转换器功能。这并没有消除任何锅炉板,而是将其全部保存在一个包含良好且可重复使用的地方。这里的版本适用于Python 3(因为这是我安装的内容!)但我认为主要的Python 2更改是用PyLong替换PyInt

int int_or_none(PyObject* o, void* i) {
    Py_ssize_t tmp;
    Py_ssize_t* i2 = i;
    if (o==Py_None) {
        return 1; // happy - leave integer as the default
    }
    if (PyLong_Check(o)) {
        tmp = PyLong_AsSize_t(o);
        if (PyErr_Occurred()) {
           return 0;
        } else {
           *i2 = tmp;
           return 1;
        }
    }
    PyErr_SetString(PyExc_TypeError, "c must be int or None");
    return 0; // conversion failed
}

static PyObject* test_int_none(PyObject* self, PyObject* args) {
    char *a, *b;
    Py_ssize_t c = 0; // default value

    if (!PyArg_ParseTuple(args, "ss|O&", &a, &b, int_or_none, &c)) {
        return NULL;
    }
    printf("c_int is %i\n", c);
    return PyLong_FromSsize_t(c);
}

一些简要说明(参考您的版本):

  • 我们相信o永远不会NULL,因为它来自Python,它总会给你一个对象。
  • 如果发生故障或None我们不更改指针。这允许在调用函数中设置默认值。
  • 转换为C整数类型后,我们必须检查是否发生了错误,因为如果整数太大,可能会出现溢出错误。在这种情况下,已经设置了正确的异常,因此我们只需返回0表示失败。 (我认为这不是Python 2的一个问题,因为它使用单独的大小整数类型)

这些建议都没有真正回答所提出的问题,但它们确实提供了我认为更清洁的替代方案。

答案 1 :(得分:1)

使用converter function是一种方法。谢谢@DavidW的提示。

我确实遇到了一些问题:

  • 如果我没有传递正确的数据类型,我现在可能会导致段错误
  • 它要求在null时,int值只能是一个值(在这种情况下为0)。它不能通用化,以便在不同的情况下我想要默认值(如-1)
  • 我必须对异常消息进行硬编码(“c必须为int”),所以我不能将其重用于另一个变量

如果有人有解决方法,请将其作为答案发布。

static int int_or_none(PyObject *python, void *c) {
    int temp = 0;
    if (python) {
        if (python != PyNone) {
            if (!PyInt_Check(python)) {
                PyErr_SetString(PyExc_TypeError, "c must be int");
                return 0;
            }

            tmp = PyInt_AsSsize_t(python);
            if (tmp  0, not %i", tmp);
                return 0;
            }
        }
    }

    *((int *) c) = tmp;

    return 0;

}


static int foo(PyObject *self, PyObject *args) {
    char *a = "";
    char *b = "";
    int *c = NULL; // If I accidentally make this a char *c, it may segfault

    if (!PyArg_ParseTuple(args, "ss|O&", &a, &b, &int_or_none, &c) {
        return NULL;
    }

    printf("c_int is %i\n", c_int);

    //some_function_requiring_int_data_type(c_int);
}