SWIG-интерфейс C-библиотеки для Python (Создание 'итерируемого' типа данных Python из структуры C 'sequence') - PullRequest
7 голосов
/ 08 января 2012

Я написал расширение Python для библиотеки C. У меня есть структура данных, которая выглядит следующим образом:

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

Назначение этого типа данных напрямую сопоставляется с типом данных списка в Python. Поэтому я хочу создать «похожее на список» поведение для экспортируемой структуры, чтобы код, написанный с использованием моего расширения C, был более «Pythonic».

В частности, это то, что я хочу сделать (из кода Python) Примечание: py_ctsruct - это тип данных ctsruct, доступ к которому осуществляется в python.

Мои требования могут быть обобщены как:

  1. list (py_ctsruct) возвращает список python со всем содержимым, скопированным из структуры c
  2. py_cstruct [i] возвращает ith элемент (предпочтительно выбрасывает IndexError в недопустимый индекс)
  3. для элемента в py_ctsruct: возможность перечисления

Согласно PEP234 , Объект может быть повторен с помощью for, если он реализует _ iter _ () или _ getitem _ () . Используя эту логику, я думаю, что, добавив следующие атрибуты (через rename ) в мой файл интерфейса SWIG, я получу желаемое поведение (кроме требования № 1 выше - которое я до сих пор не знаю, как достичь):

__len__
__getitem__
__setitem__

Теперь я могу индексировать объект C в python. Я еще не реализовал выдачу исключений Python, однако, если границы массивов превышены, возвращаются магические числа (код ошибки).

Интересно то, что когда я пытаюсь перебрать структуру, используя синтаксис 'for x in', например:

for i in py_cstruct:
    print i

Python входит в бесконечный цикл, который просто печатает магическое число (ошибка), упомянутое выше, на консоли. что подсказывает мне, что с индексацией что-то не так.

И последнее, но не менее важное. Как я могу выполнить требование 1? это включает (насколько я понимаю):

  • обработка 'вызова функции list () из python
  • Возвращение типа данных Python (список) из кода C

[[Update]]

Мне было бы интересно посмотреть небольшой фрагмент кода о том, какие (если таковые имеются) объявления, которые мне нужно поместить в мой интерфейсный файл, чтобы я мог перебирать элементы структуры c из Python.

Ответы [ 4 ]

17 голосов
/ 12 января 2012

Самое простое решение для этого - реализовать __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__() и вернуть новый итератор, но в дополнение к 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__(), который возвращал копию контейнера и next(), аналогичный первый тип. Я не делал этого в своем первоначальном ответе, потому что думал, что это будет менее понятно, чем два разных типа - контейнер и итератор для этого контейнера)

1 голос
/ 10 февраля 2017

Я столкнулся с той же проблемой с 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
}

Суть в том, что этот фрагмент полностью пропускает вызовы 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;
1 голос
/ 08 января 2012
  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 ** в C ++ в виде списка строк Python.Я предположил бы, что был бы подобный функционал.
  2. Кроме того, вы можете определить% pythoncode в вашем интерфейсе внутри структуры внутри файла swig "i".Это позволит вам добавить методы python в объект, который создается для структуры.Есть другая команда% addmethod (я думаю), которая позволяет вам добавлять методы в структуру или класс.Затем вы можете создать методы для индексации объектов в C ++ или C, если хотите.Есть много способов решить эту проблему.

Для интерфейса, над которым я работаю, я использовал объект класса, у которого есть несколько методов для доступа к данным в моем коде.Эти методы написаны на C ++.Затем я использовал директиву% pythoncode внутри класса внутри файла "i" и создал методы " getitem " и " setitem " в коде Python, который использует методы expose C ++ для созданияэто похоже на доступ в стиле словаря.

0 голосов
/ 12 января 2012

Вы говорите, что вам еще предстоит реализовать исключение 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;

Вот еще один отличный ответ для дальнейшего чтения по этой проблеме: Каксоздать генератор / итератор с Python C API?

...