【问题标题】:SWIG interfacing C library to Python (Creating 'iterable' Python data type from C 'sequence' struct)SWIG 将 C 库连接到 Python(从 C 'sequence' 结构创建'iterable' Python 数据类型)
【发布时间】:2012-02-05 06:34:41
【问题描述】:

我为 C 库编写了 Python 扩展。我有一个如下所示的数据结构:

typedef struct _mystruct{
   double * clientdata;
   size_t   len;
} MyStruct;

此数据类型的用途直接映射到 Python 中的列表数据类型。因此,我想为导出的结构创建“类似列表”的行为,以便使用我的 C 扩展编写的代码更“Pythonic”。

特别是,这是我想要做的(来自 python 代码) 注意:py_ctsruct 是在 python 中访问的 ctsruct 数据类型。

我的需求可以概括为:

  1. list(py_ctsruct) 返回一个 python 列表,其中所有内容都从 c 结构中复制出来
  2. py_cstruct[i] 返回 ith 元素(最好在无效索引上抛出 IndexError)
  3. 对于 py_ctsruct 中的元素:枚举能力

根据PEP234一个对象可以用“for”迭代,如果它实现 _iter_() 或 _getitem_()。然后使用该逻辑,我认为通过将以下属性(通过rename)添加到我的 SWIG 接口文件中,我将获得所需的行为(除了上面的 req.#1 - 我仍然不知道如何实现):

__len__
__getitem__
__setitem__

我现在可以在 python 中索引 C 对象。我还没有实现 Python 异常抛出,但是如果超出数组边界,则返回一个幻数(错误代码)。

有趣的是,当我尝试使用“for x in”语法对结构进行迭代时:

for i in py_cstruct:
    print i

Python 进入一个无限循环,该循环只是在控制台上打印上面提到的魔法(错误)数字。这表明索引有问题。

最后但并非最不重要的一点是,我该如何实现要求 1?这涉及(据我了解):

  • 处理'来自python的函数调用list()
  • 从 C 代码返回 Python(列表)数据类型

[[更新]]

我有兴趣查看一些关于我需要在接口文件中放入什么(如果有)声明的代码 sn-p,以便我可以从 Python 迭代 c 结构的元素。

【问题讨论】:

    标签: python c swig


    【解决方案1】:

    对此最简单的解决方案是实现__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 同时定义和包装,因为它不是一部分普通 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() 返回下一项并递增迭代器之外,迭代器本身还必须提供一个 @ 987654351@ 方法。这意味着容器或迭代器可以相同地使用。

    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()第一种类型。我在最初的答案中没有这样做,因为我认为这比拥有两种不同的类型(容器和该容器的迭代器)更清晰)

    【讨论】:

    • 感谢 sn-ps。快速提问,您是否从 Python 端对此进行了测试?我不太了解类型映射和 PyList/PyTuple,但我怀疑它们更接近我想要实现的目标(即将 C 结构视为 Python 序列类型)。然而,主要的一点是,我的主要问题是我无法遍历 C 结构中的元素。也许将 C 结构暴露为列表或元组有助于杀死两只鸟(迭代和数组边界检查)。这就是我要解决的两个问题——typemap + PyList 是前进的方向吗?你怎么看?
    • @HomunculusReticulli - 我使用我展示的 test() 函数测试了 Python 端的所有内容,尽管对于类型映射我只测试了 %typemap(out),但两种方法都适用于 @987654359 @Python 构造。
    • @HomunculusReticulli - 我还在我的答案中添加了__iter__() 变体,尽管在这种情况下我更喜欢__getitem__() 版本。
    • +1 获取有用的 sn-ps 和到目前为止的详细说明。我有 3 个与代码有关的问题。 1. 您能否解释一下仅在实现 getitem 方法的情况下如何进行迭代。幕后发生了什么? 2 我修改了 getitem 方法以强制执行正整数索引 - 但是当我使用负索引进行测试时,我得到一个 OverFlowError 而不是一个 IndexError - 为什么? 3. MyStructIter 具有与其迭代的 MyStruct 完全相同的字段。这是迭代器结构的要求吗?
    • @HomunculusReticulli 1 - 只有__getitem__() 需要引发 IndexError 异常,for 循环才能工作。 2 - __getitem__ 在我的示例中采用 size_t 作为索引。这是无符号的,所以在 C 端永远不能是负数。如果 SWIG 发现它在 Python 方面是负面的,它将引发异常,而不是执行笨拙的unsigned i = -1,这只会给你一个非常大的数字。尝试使用 ssize_t 代替签名版本。 3 - 这不是必需的,但我认为在这种情况下这是最简单的方法。我会做一个编辑解释更多。
    【解决方案2】:
    1. 使用 %typemap swig 命令查找。 http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps_nn25 memberin 类型映射可能会执行您想要的操作。 http://www.swig.org/Doc2.0/SWIGDocumentation.html#Typemaps_nn35 我在 Python 部分找到了一个类型映射,它允许我将 char** 数据作为 Python 字符串列表传输到 C++ 中。我猜会有类似的功能。
    2. 此外,您可以在 swig "i" 文件中的结构内的界面中定义 %pythoncode。这将允许您在为结构创建的对象中添加 python 方法。还有另一个命令 %addmethod (我认为)允许您向结构或类添加方法。然后,如果需要,您可以创建用于索引 C++ 或 C 中的对象的方法。有很多方法可以解决这个问题。

    对于我正在处理的接口,我使用了一个类对象,该对象具有一些用于访问我的代码中的数据的方法。这些方法是用 C++ 编写的。然后我在“i”文件的类中使用了 %pythoncode 指令,并在 Python 代码中创建了“getitem”和“setitem”方法,这些方法使用了暴露 C++ 方法使其看起来像字典样式的访问。

    【讨论】:

    • +1 表示 %pythoncode 建议。它允许我实现一些其他功能(访问对象属性)。不过,我仍然无法(明智地)在 Python 中迭代我的 C 结构。
    • 我认为您需要对结构中的数据进行类型映射。我包括了一些链接。 memberin 函数可能会执行您的想法。您可能需要编写一些 C 代码函数来操作对象数组并在 %pythoncode 方法中使用这些函数。如果您需要更多想法,请告诉我。
    【解决方案3】:

    你说你还没有实现 Python 异常抛出——这就是问题所在。来自 PEP 234:

    定义了一个新的异常,StopIteration,它可以用来表示迭代的结束。

    您必须在迭代结束时设置此异常。由于您的代码没有这样做,因此您遇到了您所描述的情况:

    1. 解释器循环遍历列表的自定义 iternext 函数
    2. 您的函数到达数组的末尾,而不是正确设置 StopIteration 异常,而是简单地返回您的“幻数”。
    3. 解释器认为没有充分的理由停止迭代,只是继续打印iternext...返回的值。对于解释器来说,它只是另一个列表成员。

    幸运的是,这是一个非常简单的修复方法,虽然看起来并不那么简单,因为 C 没有异常工具。 Python C API 仅使用您在引发异常情况时设置的全局错误指示符,然后 API 标准要求您将 NULL 一直返回到堆栈中的解释器,然后查看 PyErr_Occurred() 的输出到查看是否设置了错误,如果是,则打印相关的异常和回溯。

    所以在你的函数中,当你到达数组的末尾时,你只需要这个:

    PyErr_SetString(PyExc_StopIteration,"End of list");
    return NULL;
    

    这是关于此问题的进一步阅读的另一个很好的答案:How to create a generator/iterator with the Python C API?

    【讨论】:

      【解决方案4】:

      我在 Python 2.6 中遇到了同样的问题,感谢@aphex 的回复解决了这个问题。 但我想避免任何魔法值,或额外的布尔值来传递列表结束条件。果然,我的迭代器有一个 atEnd() 方法告诉我我已经超出了列表的末尾。

      所以事实上,使用 SWIG 异常处理相当容易。我只需要添加以下魔法:

      %ignore MyStructIter::atEnd();
      %exception MyStructIter::next {
          if( $self->list->atEnd() ) {
              PyErr_SetString(PyExc_StopIteration,"End of list");
              SWIG_fail;
          }
          $action
      }
      

      关键是,一旦您超过列表末尾,此片段将完全跳过 next() 调用。

      如果你坚持你的习惯用法,它应该是这样的:

      %exception 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;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多