如何允许 None 或特定类型作为 Python C 扩展函数的参数?
How to permit None or a specific type as an argument to a Python C Extension function?
假设我有一个人为设计的函数,如下所示:
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);
}
我希望用户能够提交 int
或 None
作为 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)) {
PyErr_SetString(PyExc_TypeError, "c must be int or None");
return;
}
c_int = PyInt_AsSize_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
使用 converter function 是一种方法。感谢@DavidW 的提示。
我确实有一些问题:
- 如果我没有传入正确的数据类型,我现在很容易导致段错误
- 它要求在 null 上,int 值只能是一个值(在本例中为 0)。它不能被通用化,以至于在不同的情况下我想要默认值(比如 -1)
- 我必须对异常消息进行硬编码 ("c must be int"),所以我不能真正将其重新用于不同的变量
如果有人有解决方法,请post作为答案。
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);
}
我的第一个建议是仅使用关键字参数。这样做的主要优点是避免必须传入 None
占位符值,因为您永远不必 "fill in"(比方说)一个未指定的第三个位置参数,这样您就可以指定第四个。它基本上将 Python 接口更改为 "match what you mean" 多一点。
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)
缺点是该方法并不总是有效 - 有时您需要函数符合第 3 方库强制执行的固定接口。
我的第二个建议是使用转换器函数。这并没有消除任何样板,而是将其全部保存在一个妥善包含的 re-usable 位置。这里的版本适用于 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 整数类型后,我们必须检查是否发生错误,因为如果整数太大,可能会出现溢出错误。在这种情况下,已经设置了正确的异常,所以我们只需要 return 0 来指示失败。 (我认为这与 Python 2 无关,因为它使用单独的大整数和小整数类型)
这些建议都没有真正回答所提出的问题,但它们确实提供了我认为更清洁的替代方案。
假设我有一个人为设计的函数,如下所示:
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); }
我希望用户能够提交 int
或 None
作为 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)) { PyErr_SetString(PyExc_TypeError, "c must be int or None"); return; } c_int = PyInt_AsSize_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
使用 converter function 是一种方法。感谢@DavidW 的提示。
我确实有一些问题:
- 如果我没有传入正确的数据类型,我现在很容易导致段错误
- 它要求在 null 上,int 值只能是一个值(在本例中为 0)。它不能被通用化,以至于在不同的情况下我想要默认值(比如 -1)
- 我必须对异常消息进行硬编码 ("c must be int"),所以我不能真正将其重新用于不同的变量
如果有人有解决方法,请post作为答案。
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); }
我的第一个建议是仅使用关键字参数。这样做的主要优点是避免必须传入 None
占位符值,因为您永远不必 "fill in"(比方说)一个未指定的第三个位置参数,这样您就可以指定第四个。它基本上将 Python 接口更改为 "match what you mean" 多一点。
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)
缺点是该方法并不总是有效 - 有时您需要函数符合第 3 方库强制执行的固定接口。
我的第二个建议是使用转换器函数。这并没有消除任何样板,而是将其全部保存在一个妥善包含的 re-usable 位置。这里的版本适用于 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 整数类型后,我们必须检查是否发生错误,因为如果整数太大,可能会出现溢出错误。在这种情况下,已经设置了正确的异常,所以我们只需要 return 0 来指示失败。 (我认为这与 Python 2 无关,因为它使用单独的大整数和小整数类型)
这些建议都没有真正回答所提出的问题,但它们确实提供了我认为更清洁的替代方案。