SWIG将C库接口连接Python(从C 'sequence'结构创建可迭代的Python数据类型)
我写了一个Python扩展,用来和一个C语言库配合使用。我有一个数据结构,长得像这样:
typedef struct _mystruct{
double * clientdata;
size_t len;
} MyStruct;
这个数据类型的目的和Python里的列表类型直接对应。因此,我想为导出的结构体创建一种“像列表一样”的行为,这样用我的C扩展写的代码就会更符合Python的风格。
具体来说,我希望能在Python代码中做到以下几点: 注意:py_cstruct是一个在Python中访问的ctstruct数据类型。
我的需求可以总结为:
- list(py_cstruct) 返回一个Python列表,里面包含从C结构体中复制出来的所有内容
- py_cstruct[i] 返回第i个元素(如果索引无效,最好能抛出IndexError错误)
- for elem in py_cstruct: 能够遍历元素
根据PEP234,一个对象可以通过“for”循环进行迭代,如果它实现了 _iter_() 或 _getitem_() 方法。按照这个逻辑,我认为通过在我的SWIG接口文件中添加以下属性(通过rename)可以实现我想要的行为(除了上面提到的第1个需求,我还不知道怎么实现):
__len__
__getitem__
__setitem__
现在我可以在Python中对C对象进行索引了。不过我还没有实现Python异常的抛出,如果数组越界的话,我会返回一个魔法数字(错误代码)。
有趣的是,当我尝试用“for x in”语法遍历这个结构体时,例如:
for i in py_cstruct:
print i
Python进入了一个无限循环,只是在控制台上打印出上面提到的魔法(错误)数字,这让我觉得索引可能有问题。
最后,我该如何实现第1个需求呢?我理解这涉及到:
- 处理来自Python的list()函数调用
- 从C代码返回一个Python(列表)数据类型
[[更新]]
我希望能看到一些代码片段,告诉我在接口文件中需要添加哪些声明,以便我可以从Python遍历C结构体的元素。
4 个回答
你提到还没有实现Python的异常抛出,这就是问题所在。根据PEP 234的说明:
定义了一个新的异常,叫做StopIteration,用来表示迭代的结束。
你必须在迭代结束时设置这个异常。因为你的代码没有做到这一点,所以你遇到了你描述的情况:
- 解释器在你的列表的自定义
iternext
函数中循环。 - 你的函数到达数组的末尾,而不是正确地设置
StopIteration
异常,而是简单地返回了你的“魔法数字”。 - 解释器看到没有理由停止迭代,就继续打印
iternext
返回的值……你的魔法数字。对解释器来说,这只是列表中的另一个成员。
幸运的是,这个问题的解决方法相对简单,尽管看起来可能不那么直接,因为C语言没有异常处理的机制。Python的C API使用一个全局错误指示器,当出现异常情况时,你需要设置这个指示器,然后根据API标准,你需要一路返回NULL到解释器,解释器会查看 PyErr_Occurred()
的输出,看看是否设置了错误,如果有,就会打印相关的异常和回溯信息。
所以在你的函数中,当你到达数组的末尾时,你只需要这样做:
PyErr_SetString(PyExc_StopIteration,"End of list");
return NULL;
这里还有一个很好的答案,可以进一步阅读这个问题: 如何使用Python C API创建生成器/迭代器?
- 可以使用 %typemap 这个 swig 命令来查找相关信息。你可以在这个链接找到详细的说明:http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps。在这个文档中,有一个叫 memberin 的 typemap,可能正好能满足你的需求。还有一个链接是关于 Python 部分的 typemap,里面有一个可以把 char** 数据转成 Python 字符串列表的例子。我猜应该还有类似的功能可以用。
- 另外,你可以在 swig 的 "i" 文件中,在结构体里面定义 %pythoncode。这可以让你在创建的结构体对象中添加 Python 方法。还有一个命令叫 %addmethod(我记得是这个),可以让你给结构体或类添加方法。这样的话,你就可以在 C++ 或 C 中创建方法来索引这些对象。解决这个问题的方法有很多种。
在我正在做的一个接口中,我使用了一个类对象,这个对象有一些方法可以访问我代码中的数据。这些方法是用 C++ 写的。然后我在 "i" 文件中的类里面使用了 %pythoncode 指令,创建了 "getitem" 和 "setitem" 这两个 Python 方法,利用暴露的 C++ 方法,让它看起来像字典那样可以访问。
最简单的解决办法是实现一个叫做 __getitem__
的功能,并在索引无效时抛出一个 IndexError
异常。
我做了一个示例,使用 %extend
和 %exception
在 SWIG 中实现 __getitem__
和抛出异常:
%module test
%include "exception.i"
%{
#include <assert.h>
#include "test.h"
static int myErr = 0; // flag to save error state
%}
%exception MyStruct::__getitem__ {
assert(!myErr);
$action
if (myErr) {
myErr = 0; // clear flag for next time
// You could also check the value in $result, but it's a PyObject here
SWIG_exception(SWIG_IndexError, "Index out of bounds");
}
}
%include "test.h"
%extend MyStruct {
double __getitem__(size_t i) {
if (i >= $self->len) {
myErr = 1;
return 0;
}
return $self->clientdata[i];
}
}
我通过在 test.h 中添加内容来测试它:
static MyStruct *test() {
static MyStruct inst = {0,0};
if (!inst.clientdata) {
inst.len = 10;
inst.clientdata = malloc(sizeof(double)*inst.len);
for (size_t i = 0; i < inst.len; ++i) {
inst.clientdata[i] = i;
}
}
return &inst;
}
然后运行以下 Python 代码:
import test
for i in test.test():
print i
这段代码会打印:
python run.py
0.0
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
然后结束。
另外一种方法是使用类型映射,将 MyStruct
直接映射到 PyList
也是可以的:
%module test
%{
#include "test.h"
%}
%typemap(out) (MyStruct *) {
PyObject *list = PyList_New($1->len);
for (size_t i = 0; i < $1->len; ++i) {
PyList_SetItem(list, i, PyFloat_FromDouble($1->clientdata[i]));
}
$result = list;
}
%include "test.h"
这样会创建一个 PyList
,其返回值来自任何返回 MyStruct *
的函数。我用和之前方法完全相同的函数测试了这个 %typemap(out)
。
你也可以为反向操作编写相应的 %typemap(in)
和 %typemap(freearg)
,像这样未测试的代码:
%typemap(in) (MyStruct *) {
if (!PyList_Check($input)) {
SWIG_exception(SWIG_TypeError, "Expecting a PyList");
return NULL;
}
MyStruct *tmp = malloc(sizeof(MyStruct));
tmp->len = PyList_Size($input);
tmp->clientdata = malloc(sizeof(double) * tmp->len);
for (size_t i = 0; i < tmp->len; ++i) {
tmp->clientdata[i] = PyFloat_AsDouble(PyList_GetItem($input, i));
if (PyErr_Occured()) {
free(tmp->clientdata);
free(tmp);
SWIG_exception(SWIG_TypeError, "Expecting a double");
return NULL;
}
}
$1 = tmp;
}
%typemap(freearg) (MyStruct *) {
free($1->clientdata);
free($1);
}
对于像链表这样的容器,使用迭代器会更合适,但为了完整性,这里是如何为 MyStruct
实现 __iter__
的方法。关键是让 SWIG 为你包装另一种类型,这种类型提供了需要的 __iter__()
和 next()
,在这个例子中是 MyStructIter
,它通过 %inline
同时定义和包装,因为它不是正常 C API 的一部分:
%module test
%include "exception.i"
%{
#include <assert.h>
#include "test.h"
static int myErr = 0;
%}
%exception MyStructIter::next {
assert(!myErr);
$action
if (myErr) {
myErr = 0; // clear flag for next time
PyErr_SetString(PyExc_StopIteration, "End of iterator");
return NULL;
}
}
%inline %{
struct MyStructIter {
double *ptr;
size_t len;
};
%}
%include "test.h"
%extend MyStructIter {
struct MyStructIter *__iter__() {
return $self;
}
double next() {
if ($self->len--) {
return *$self->ptr++;
}
myErr = 1;
return 0;
}
}
%extend MyStruct {
struct MyStructIter __iter__() {
struct MyStructIter ret = { $self->clientdata, $self->len };
return ret;
}
}
关于 容器的迭代 的要求是,容器需要实现 __iter__()
并返回一个新的迭代器,此外,迭代器本身也必须提供一个 __iter__()
方法。这意味着容器或迭代器可以以相同的方式使用。
MyStructIter
需要跟踪当前的迭代状态——我们在哪里,剩下多少。在这个例子中,我通过保持指向下一个项目的指针和一个计数器来做到这一点,用于判断何时到达末尾。你也可以通过保持指向迭代器正在使用的 MyStruct
的指针和一个位置计数器来跟踪状态,像这样:
%inline %{
struct MyStructIter {
MyStruct *list;
size_t pos;
};
%}
%include "test.h"
%extend MyStructIter {
struct MyStructIter *__iter__() {
return $self;
}
double next() {
if ($self->pos < $self->list->len) {
return $self->list->clientdata[$self->pos++];
}
myErr = 1;
return 0;
}
}
%extend MyStruct {
struct MyStructIter __iter__() {
struct MyStructIter ret = { $self, 0 };
return ret;
}
}
(在这个情况下,我们实际上可以直接使用容器本身作为迭代器,通过提供一个返回容器的 __iter__()
的方法和一个类似于第一种类型的 next()
。我在最初的回答中没有这样做,因为我觉得这样会比有两个不同的类型——一个容器和一个为该容器服务的迭代器——更不清晰。)