我为C库编写了一个Python扩展。我有一个如下所示的数据结构:
typedef struct _mystruct{
double * clientdata;
size_t len;
} MyStruct;
此数据类型的目的直接映射到Python中的列表数据类型。因此,我想为导出的结构创建“类似列表”的行为,因此使用我的C扩展编写的代码更像是'Pythonic'。
特别是,这是我想要做的(来自python代码) 注意:py_ctsruct是在python中访问的ctsruct数据类型。
我的要求可以归结为:
根据PEP234,一个对象可以用“for”迭代,如果它实现的话 _ iter _()或_ getitem _()。然后使用该逻辑,我认为通过将以下属性(通过rename)添加到我的SWIG接口文件,我将获得所需的行为(除了上面的请求#1 - 我仍然不知道如何实现):
__len__
__getitem__
__setitem__
我现在能够在python中索引C对象。我还没有实现Python异常抛出,但是如果超出数组边界,则返回一个幻数(错误代码)。
有趣的是,当我尝试使用'for x in'语法迭代结构时,例如:
for i in py_cstruct:
print i
Python进入一个无限循环,它只是在控制台上打印上面提到的魔术(错误)数字。这告诉我索引有问题。
最后但并非最不重要,我如何实施要求1?这涉及(据我所知):
[[更新]]
我有兴趣看一下我需要在我的接口文件中放入什么(如果有的话)声明的一些代码片段,以便我可以从Python迭代c结构的元素。
答案 0 :(得分:17)
最简单的解决方案是实现__getitem__
并为无效索引抛出IndexError
例外。
我汇总了一个示例,使用SWIG中的%extend
和%exception
来实现__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
{1}}因为它不是普通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;
}
}
iteration over containers的要求是容器需要实现__iter__()
并返回一个新的迭代器,但除了next()
之外还返回下一个项并递增迭代器迭代器本身也必须提供__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__()
和{{1类似于第一种类型。我没有在我的原始答案中这样做,因为我认为这不会比有两种不同的类型 - 容器和该容器的迭代器那样清晰。
答案 1 :(得分:1)
对于我正在处理的接口,我使用了一个类对象,它有一些方法可以访问我的代码中的数据。这些方法是用C ++编写的。然后我在“i”文件里面的类中使用了%pythoncode指令,并在使用公开C ++方法的Python代码中创建了“ getitem ”和“ setitem ”方法使它看起来像字典样式访问。
答案 2 :(得分:1)
我遇到了与 Python 2.6 相同的问题,并且感谢@aphex回复解决了这个问题。 但我想避免任何魔术值,或额外的布尔值来传递列表结束条件。果然,我的迭代器有一个 atEnd()方法,告诉我我已经过了列表的末尾。
事实上,SWIG异常处理相当容易。我只需添加以下魔法:
%ignore MyStructIter::atEnd();
%except MyStructIter::next {
if( $self->list->atEnd() ) {
PyErr_SetString(PyExc_StopIteration,"End of list");
SWIG_fail;
}
$action
}
重点是,一旦超过列表末尾,snipet将完全跳过next()调用。
如果你坚持自己的习语,它应该看起来像:
%except MyStructIter::next {
if( $self->pos >= $self->list->len ) {
PyErr_SetString(PyExc_StopIteration,"End of list");
SWIG_fail;
}
$action
}
注意 PYTHON 3.x :
您应使用神奇的“__”前缀和后缀名称命名 next()函数。一个选项只是添加:
%rename(__next__) MyStructIter::next;
答案 3 :(得分:0)
你说你还没有实现Python异常抛出 - 这就是问题所在。来自PEP 234:
定义了一个新的异常StopIteration,它可以用来表示迭代的结束。
您必须在迭代结束时设置此异常。由于您的代码不会执行此操作,因此您遇到了您所描述的情况:
iternext
函数StopIteration
异常,只需返回您的“幻数”。iternext
...你的幻数返回的值。对于口译员来说,这只是另一个名单成员。幸运的是,这是一个非常简单的修复,但可能看起来并不那么简单,因为C没有异常功能。 Python C API只使用在引发异常情况时设置的全局错误指示符,然后API标准规定您在堆栈中一直返回NULL到解释器,然后解释器查看{{1}的输出查看是否设置了错误,如果是,则打印相关的异常和回溯。
所以在你的函数中,当你到达数组的末尾时,你只需要这个:
PyErr_Occurred()
以下是进一步阅读此问题的另一个很好的答案:How to create a generator/iterator with the Python C API?