Объединение массивов в Boost Python

У меня есть ряд структур С++, которые я пытаюсь обернуть, используя boost python. Я столкнулся с трудностями, когда эти структуры содержат массивы. Я пытаюсь сделать это с минимальными издержками и, к сожалению, я не могу вносить какие-либо изменения в сами структуры. Так например, скажем, у меня

struct Foo
{
    int vals[3];
};

Я хотел бы иметь доступ к этому в python следующим образом:

f = Foo()
f.vals[0] = 10
print f.vals[0]

Сейчас я использую ряд функций get/set, который работает, но очень неэлегантен и несовместим с доступом к другим членам, не относящимся к массиву. Вот мое текущее решение:

int getVals (Foo f, int index) { return f.vals[index]; }
void setVals (Foo& f, int index, int value) { f.vals[index] = value; }

boost::python::class_< Foo > ( "Foo", init<>() )
    .def ( "getVals", &getVals )
    .def ( "setVals", &setVals );

Мне хорошо, что у меня есть функции get/set (так как есть определенные случаи, когда мне нужно реализовать пользовательскую операцию get или set), но я не уверен, как включить оператор [] для доступа к элементам массива, В других классах, которые сами доступны с помощью оператора [], я смог использовать _getitem _ и _setitem _, которые отлично работали, но я не уверен, как это сделать это с членами класса, если это возможно даже.

Ответы

Ответ 1

Для такого относительно простого вопроса ответ становится довольно привлекательным. Прежде чем предоставить решение, сначала рассмотрим глубину проблемы:

 f = Foo()
 f.vals[0] = 10

f.vals возвращает промежуточный объект, который предоставляет методы __getitem__ и __setitem__. Для поддержки Boost.Python для каждого типа массива необходимо будет использовать вспомогательные типы, и эти типы обеспечат поддержку индексирования.

Одним из тонких различий между языками является время жизни объекта. Рассмотрим следующее:

 f = Foo()
 v = f.vals
 f = None
 v[0] = 10

Когда время жизни объекта Python управляется посредством подсчета ссылок, f не владеет объектом, на который ссылается vals. Следовательно, хотя объект, на который ссылается f, уничтожается, когда f установлен на None, объект, на который ссылается v, остается в силе. Это противоречит типу С++ Foo, который должен быть открыт, поскольку Foo владеет памятью, к которой относится vals. С Boost.Python вспомогательный объект, возвращаемый f.vals, должен продлить срок службы объекта, на который ссылается f.


С рассмотренной проблемой давайте начнем с решения. Ниже приведены основные массивы, которые необходимо просмотреть:

struct Foo
{
  int vals[3];
  boost::array<std::string, 5> strs;

  Foo()  { std::cout << "Foo()"  << std::endl; }
  ~Foo() { std::cout << "~Foo()" << std::endl; }
};

int more_vals[2];

Вспомогательный тип для Foo::vals и Foo::strs должен обеспечивать минимальные накладные расходы, поддерживая индексацию. Это выполняется в array_proxy:

/// @brief Type that proxies to an array.
template <typename T>
class array_proxy
{
public:
  // Types
  typedef T           value_type;
  typedef T*          iterator;
  typedef T&          reference;
  typedef std::size_t size_type;

  /// @brief Empty constructor.
  array_proxy()
    : ptr_(0),
      length_(0)
  {}

  /// @brief Construct with iterators.
  template <typename Iterator>
  array_proxy(Iterator begin, Iterator end)
    : ptr_(&*begin),
      length_(std::distance(begin, end))
  {}

  /// @brief Construct with with start and size.
  array_proxy(reference begin, std::size_t length)
    : ptr_(&begin),
      length_(length)
  {}

  // Iterator support.
  iterator begin()               { return ptr_; }
  iterator end()                 { return ptr_ + length_; }

  // Element access.
  reference operator[](size_t i) { return ptr_[i]; }

  // Capacity.
  size_type size()               { return length_; }

private:
  T* ptr_;
  std::size_t length_;
};

При выполнении вспомогательного типа оставшаяся часть должна добавить возможность раскрывать возможности индексирования вспомогательному типу в Python. Boost.Python indexing_suite предоставляет перехваты для добавления поддержки индексирования к открытым типам с помощью подхода на основе политики. Ниже приведен класс ref_index_suite - это класс политики, удовлетворяющий требованиям типа DerivedPolicies:

/// @brief Policy type for referenced indexing, meeting the DerivedPolicies
///        requirement of boost::python::index_suite.
/// 
/// @note Requires Container to support:
///          - value_type and size_type types,
///          - value_type is default constructable and copyable,
///          - element access via operator[],
///          - Default constructable, iterator constructable,
///          - begin(), end(), and size() member functions
template <typename Container>
class ref_index_suite
  : public boost::python::indexing_suite<Container,
      ref_index_suite<Container> >

{
public:

  typedef typename Container::value_type data_type;
  typedef typename Container::size_type  index_type;
  typedef typename Container::size_type  size_type;

  // Element access and manipulation.

  /// @brief Get element from container.
  static data_type&
  get_item(Container& container, index_type index)
  {
    return container[index];
  }

  /// @brief Set element from container.
  static void
  set_item(Container& container, index_type index, const data_type& value)
  {
    container[index] = value;
  }

  /// @brief Reset index to default value.
  static void
  delete_item(Container& container, index_type index)
  {
    set_item(container, index, data_type());
  };

  // Slice support.

  /// @brief Get slice from container.
  ///
  /// @return Python object containing
  static boost::python::object
  get_slice(Container& container, index_type from, index_type to)
  {
    using boost::python::list;
    if (from > to) return list();

    // Return copy, as container only references its elements.
    list list;
    while (from != to) list.append(container[from++]);
    return list;
  };

  /// @brief Set a slice in container with a given value.
  static void
  set_slice(
    Container& container, index_type from,
    index_type to, const data_type& value
  )
  {
    // If range is invalid, return early.
    if (from > to) return;

    // Populate range with value.
    while (from < to) container[from++] = value;
  }

  /// @brief Set a slice in container with another range.
  template <class Iterator>
  static void
  set_slice(
    Container& container, index_type from,
    index_type to, Iterator first, Iterator last
  )
  {
    // If range is invalid, return early.
    if (from > to) return;

    // Populate range with other range.
    while (from < to) container[from++] = *first++;   
  }

  /// @brief Reset slice to default values.
  static void
  delete_slice(Container& container, index_type from, index_type to)
  {
    set_slice(container, from, to, data_type());
  }

  // Capacity.

  /// @brief Get size of container.
  static std::size_t
  size(Container& container) { return container.size(); }

  /// @brief Check if a value is within the container.
  template <class T>
  static bool
  contains(Container& container, const T& value)
  {
    return std::find(container.begin(), container.end(), value)
        != container.end();
  }

  /// @brief Minimum index supported for container.
  static index_type
  get_min_index(Container& /*container*/)
  {
      return 0;
  }

  /// @brief Maximum index supported for container.
  static index_type
  get_max_index(Container& container)
  {
    return size(container);
  }

  // Misc.

  /// @brief Convert python index (could be negative) to a valid container
  ///        index with proper boundary checks.
  static index_type
  convert_index(Container& container, PyObject* object)
  {
    namespace python = boost::python;
    python::extract<long> py_index(object);

    // If py_index cannot extract a long, then type the type is wrong so
    // set error and return early.
    if (!py_index.check()) 
    {
      PyErr_SetString(PyExc_TypeError, "Invalid index type");
      python::throw_error_already_set(); 
      return index_type();
    }

    // Extract index.
    long index = py_index();

    // Adjust negative index.
    if (index < 0)
        index += container.size();

    // Boundary check.
    if (index >= long(container.size()) || index < 0)
    {
      PyErr_SetString(PyExc_IndexError, "Index out of range");
      python::throw_error_already_set();
    }

    return index;
  }
};

Каждый вспомогательный тип должен быть открыт через Boost.Python с помощью boost::python::class_<...>. Это может быть немного утомительным, поэтому одна вспомогательная функция будет условно регистрировать типы.

/// @brief Conditionally register a type with Boost.Python.
template <typename T>
void register_array_proxy()
{
  typedef array_proxy<T> proxy_type;

  // If type is already registered, then return early.
  namespace python = boost::python;
  bool is_registered = (0 != python::converter::registry::query(
    python::type_id<proxy_type>())->to_python_target_type());
  if (is_registered) return;

  // Otherwise, register the type as an internal type.
  std::string type_name = std::string("_") + typeid(T).name();
  python::class_<proxy_type>(type_name.c_str(), python::no_init)
    .def(ref_index_suite<proxy_type>());
}

Кроме того, вывод аргумента шаблона будет использоваться для предоставления пользователю простого API:

/// @brief Create a callable Boost.Python object from an array.
template <typename Array>
boost::python::object make_array(Array array)
{
  // Deduce the array_proxy type by removing all the extents from the
  // array.
  ...

  // Register an array proxy.
  register_array_proxy<...>();
}

При обращении к Python Foo::vals необходимо преобразовать из int[3] в array_proxy<int>. Класс шаблона может служить функтором, который создаст array_proxy соответствующего типа. Ниже приведена array_proxy_getter.

/// @brief Functor used used convert an array to an array_proxy for
///        non-member objects.
template <typename NativeType,
          typename ProxyType>
struct array_proxy_getter
{
public:
  typedef NativeType native_type;
  typedef ProxyType  proxy_type;

  /// @brief Constructor.
  array_proxy_getter(native_type array): array_(array) {}

  /// @brief Return an array_proxy for a member array object.
  template <typename C>
  proxy_type operator()(C& c) { return make_array_proxy(c.*array_); }

  /// @brief Return an array_proxy for non-member array object.
  proxy_type operator()() { return make_array_proxy(*array_); }
private:
  native_type array_;
};

Экземпляры этого функтора будут завернуты в вызываемый boost::python::object. Единая точка входа make_array расширяется:

/// @brief Create a callable Boost.Python object from an array.
template <typename Array>
boost::python::object make_array(Array array)
{ 
  // Deduce the array_proxy type by removing all the extents from the
  // array.
  ...

  // Register an array proxy.
  register_array_proxy<...>();

  // Create function.
  return boost::python::make_function(
      array_proxy_getter<Array>(array),
      ...);
}

Наконец, нужно управлять временем жизни объекта. Boost.Python предоставляет привязки для определения того, как управлять жизненными циклами объекта с помощью концепции CallPolices. В этом случае with_custodian_and_ward_postcall можно использовать для обеспечения того, чтобы array_proxy<int>, возвращаемый из foo_vals(), продлевал срок жизни экземпляра Foo из которого он был создан.

// CallPolicy type used to keep the owner alive when returning an object
// that references the parents member variable.
typedef boost::python::with_custodian_and_ward_postcall<
    0, // return object (custodian)
    1  // self or this (ward)
  > return_keeps_owner_alive;

Ниже приведен полный пример поддержки не-членных и членных родных и Boost.Array одноразмерных массивов:

#include <string>
#include <typeinfo>
#include <boost/python.hpp>
#include <boost/python/suite/indexing/indexing_suite.hpp>

namespace detail {

template <typename> struct array_trait;

/// @brief Type that proxies to an array.
template <typename T>
class array_proxy
{
public:
  // Types
  typedef T           value_type;
  typedef T*          iterator;
  typedef T&          reference;
  typedef std::size_t size_type;

  /// @brief Empty constructor.
  array_proxy()
    : ptr_(0),
      length_(0)
  {}

  /// @brief Construct with iterators.
  template <typename Iterator>
  array_proxy(Iterator begin, Iterator end)
    : ptr_(&*begin),
      length_(std::distance(begin, end))
  {}

  /// @brief Construct with with start and size.
  array_proxy(reference begin, std::size_t length)
    : ptr_(&begin),
      length_(length)
  {}

  // Iterator support.
  iterator begin()               { return ptr_; }
  iterator end()                 { return ptr_ + length_; }

  // Element access.
  reference operator[](size_t i) { return ptr_[i]; }

  // Capacity.
  size_type size()               { return length_; }

private:
  T* ptr_;
  std::size_t length_;
};

/// @brief Make an array_proxy.
template <typename T>
array_proxy<typename array_trait<T>::element_type>
make_array_proxy(T& array)
{
  return array_proxy<typename array_trait<T>::element_type>(
    array[0],
    array_trait<T>::static_size);
}

/// @brief Policy type for referenced indexing, meeting the DerivedPolicies
///        requirement of boost::python::index_suite.
/// 
/// @note Requires Container to support:
///          - value_type and size_type types,
///          - value_type is default constructable and copyable,
///          - element access via operator[],
///          - Default constructable, iterator constructable,
///          - begin(), end(), and size() member functions
template <typename Container>
class ref_index_suite
  : public boost::python::indexing_suite<Container,
      ref_index_suite<Container> >
{
public:

  typedef typename Container::value_type data_type;
  typedef typename Container::size_type  index_type;
  typedef typename Container::size_type  size_type;

  // Element access and manipulation.

  /// @brief Get element from container.
  static data_type&
  get_item(Container& container, index_type index)
  {
    return container[index];
  }

  /// @brief Set element from container.
  static void
  set_item(Container& container, index_type index, const data_type& value)
  {
    container[index] = value;
  }

  /// @brief Reset index to default value.
  static void
  delete_item(Container& container, index_type index)
  {
    set_item(container, index, data_type());
  };

  // Slice support.

  /// @brief Get slice from container.
  ///
  /// @return Python object containing
  static boost::python::object
  get_slice(Container& container, index_type from, index_type to)
  {
    using boost::python::list;
    if (from > to) return list();

    // Return copy, as container only references its elements.
    list list;
    while (from != to) list.append(container[from++]);
    return list;
  };

  /// @brief Set a slice in container with a given value.
  static void
  set_slice(
    Container& container, index_type from,
    index_type to, const data_type& value
  )
  {
    // If range is invalid, return early.
    if (from > to) return;

    // Populate range with value.
    while (from < to) container[from++] = value;
  }

  /// @brief Set a slice in container with another range.
  template <class Iterator>
  static void
  set_slice(
    Container& container, index_type from,
    index_type to, Iterator first, Iterator last
  )
  {
    // If range is invalid, return early.
    if (from > to) return;

    // Populate range with other range.
    while (from < to) container[from++] = *first++;   
  }

  /// @brief Reset slice to default values.
  static void
  delete_slice(Container& container, index_type from, index_type to)
  {
    set_slice(container, from, to, data_type());
  }

  // Capacity.

  /// @brief Get size of container.
  static std::size_t
  size(Container& container) { return container.size(); }

  /// @brief Check if a value is within the container.
  template <class T>
  static bool
  contains(Container& container, const T& value)
  {
    return std::find(container.begin(), container.end(), value)
        != container.end();
  }

  /// @brief Minimum index supported for container.
  static index_type
  get_min_index(Container& /*container*/)
  {
      return 0;
  }

  /// @brief Maximum index supported for container.
  static index_type
  get_max_index(Container& container)
  {
    return size(container);
  }

  // Misc.

  /// @brief Convert python index (could be negative) to a valid container
  ///        index with proper boundary checks.
  static index_type
  convert_index(Container& container, PyObject* object)
  {
    namespace python = boost::python;
    python::extract<long> py_index(object);

    // If py_index cannot extract a long, then type the type is wrong so
    // set error and return early.
    if (!py_index.check()) 
    {
      PyErr_SetString(PyExc_TypeError, "Invalid index type");
      python::throw_error_already_set(); 
      return index_type();
    }

    // Extract index.
    long index = py_index();

    // Adjust negative index.
    if (index < 0)
        index += container.size();

    // Boundary check.
    if (index >= long(container.size()) || index < 0)
    {
      PyErr_SetString(PyExc_IndexError, "Index out of range");
      python::throw_error_already_set();
    }

    return index;
  }
};

/// @brief Trait for arrays.
template <typename T>
struct array_trait_impl;

// Specialize for native array.
template <typename T, std::size_t N>
struct array_trait_impl<T[N]>
{
  typedef T element_type;
  enum { static_size = N };
  typedef array_proxy<element_type> proxy_type;
  typedef boost::python::default_call_policies policy;
  typedef boost::mpl::vector<array_proxy<element_type> > signature;
};

// Specialize boost::array to use the native array trait.
template <typename T, std::size_t N>
struct array_trait_impl<boost::array<T, N> >
  : public array_trait_impl<T[N]>
{};

// @brief Specialize for member objects to use and modify non member traits.
template <typename T, typename C>
struct array_trait_impl<T (C::*)>
  : public array_trait_impl<T>
{
  typedef boost::python::with_custodian_and_ward_postcall<
      0, // return object (custodian)
      1  // self or this (ward)
    > policy;

  // Append the class to the signature.
  typedef typename boost::mpl::push_back<
    typename array_trait_impl<T>::signature, C&>::type signature;
};

/// @brief Trait class used to deduce array information, policies, and 
///        signatures
template <typename T>
struct array_trait:
  public array_trait_impl<typename boost::remove_pointer<T>::type>
{
  typedef T native_type;
};

/// @brief Functor used used convert an array to an array_proxy for
///        non-member objects.
template <typename Trait>
struct array_proxy_getter
{
public:
  typedef typename Trait::native_type native_type;
  typedef typename Trait::proxy_type proxy_type;

  /// @brief Constructor.
  array_proxy_getter(native_type array): array_(array) {}

  /// @brief Return an array_proxy for a member array object.
  template <typename C>
  proxy_type operator()(C& c) { return make_array_proxy(c.*array_); }

  /// @brief Return an array_proxy for a non-member array object.
  proxy_type operator()() { return make_array_proxy(*array_); }
private:
  native_type array_;
};

/// @brief Conditionally register a type with Boost.Python.
template <typename Trait>
void register_array_proxy()
{
  typedef typename Trait::element_type element_type;
  typedef typename Trait::proxy_type proxy_type;

  // If type is already registered, then return early.
  namespace python = boost::python;
  bool is_registered = (0 != python::converter::registry::query(
    python::type_id<proxy_type>())->to_python_target_type());
  if (is_registered) return;

  // Otherwise, register the type as an internal type.
  std::string type_name = std::string("_") + typeid(element_type).name();
  python::class_<proxy_type>(type_name.c_str(), python::no_init)
    .def(ref_index_suite<proxy_type>());
}

/// @brief Create a callable Boost.Python object that will return an
///        array_proxy type when called.
///
/// @note This function will conditionally register array_proxy types
///       for conversion within Boost.Python.  The array_proxy will
///       extend the life of the object from which it was called.
///       For example, if `foo` is an object, and `vars` is an array,
///       then the object returned from `foo.vars` will extend the life
///       of `foo`.
template <typename Array>
boost::python::object make_array_aux(Array array)
{
  typedef array_trait<Array> trait_type;
  // Register an array proxy.
  register_array_proxy<trait_type>();

  // Create function.
  return boost::python::make_function(
      array_proxy_getter<trait_type>(array),
      typename trait_type::policy(),
      typename trait_type::signature());
}

} // namespace detail

/// @brief Create a callable Boost.Python object from an array.
template <typename T>
boost::python::object make_array(T array)
{ 
  return detail::make_array_aux(array);
}

struct Foo
{
  int vals[3];
  boost::array<std::string, 5> strs;

  Foo()  { std::cout << "Foo()"  << std::endl; }
  ~Foo() { std::cout << "~Foo()" << std::endl; }
};

int more_vals[2];

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;

  python::class_<Foo>("Foo")
    .add_property("vals", make_array(&Foo::vals))
    .add_property("strs", make_array(&Foo::strs))
    ;
  python::def("more_vals", make_array(&more_vals));
}

И использование, тестирование доступа, нарезка, проверка типов и управление жизненным циклом:

>>> from example import Foo, more_vals
>>> def print_list(l): print ', '.join(str(v) for v in l)
... 
>>> f = Foo()
Foo()
>>> f.vals[0] = 10
>>> print f.vals[0]
10
>>> f.vals[0] = '10'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Invalid assignment
>>> f.vals[100] = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: Index out of range
>>> f.vals[:] = xrange(100,103)
>>> print_list(f.vals)
100, 101, 102
>>> f.strs[:] = ("a", "b", "c", "d", "e")
>>> print_list(f.strs)
a, b, c, d, e
>>> f.vals[-1] = 30
>>> print_list(f.vals)
100, 101, 30
>>> v = f.vals
>>> del v[:-1]
>>> print_list(f.vals)
0, 0, 30
>>> print_list(v)
0, 0, 30
>>> x = v[-1:]
>>> f = None
>>> v = None
~Foo()
>>> print_list(x)
30
>>> more_vals()[:] = xrange(50, 100)
>>> print_list(more_vals())
50, 51

Ответ 2

Я думаю, что он должен легко работать с набором индексирования boost.python. Для вашего случая вам нужно указать контейнер float* и вернуть постоянный размер в производную политику.

РЕДАКТИРОВАТЬ. Метод, описанный выше, хорош для контейнеров, но его трудно использовать для вашего случая. Самое простое - объявить две функции set и get:

int getitem_foo(Foo & f, int index)
{
  if(index < 0 || index >=3)
  {
    PyErr_SetString(PyExc_IndexError, "index out of range");
    throw boost::python::error_already_set();;
  }
  return f.vals[index];
}

void setitem_foo(Foo& f, int index, int val)
{
  if(index < 0 || index >=3)
  {
    PyErr_SetString(PyExc_IndexError, "index out of range");
    throw boost::python::error_already_set();;
  }
  f.vals[index] = val;
}

а затем:

boost::python::class_< Foo  >("Foo")
  .def("__getitem__", &getitem_foo)
  .def("__setitem__", &setitem_foo)
;

Ответ 3

Я попробую ответить @Tanner Sansbury выше. Это полезно, но есть что-то исправить. Показать здесь.

Например, существует такой класс:

struct S {
    float a;
    float b;
    bool operator==(const S& s) {
        return a == s.a && b == s.b;
    }
};

class Foo {
 public:
    bool arr[100];
    S brr[100];
}

Для пользовательского типа возникает ошибка компиляции.

Первый шаг, вы должны реализовать функцию operator == вашего пользовательского типа, как указано выше.

Второй шаг, добавьте больше кода в класс array_proxy.

/// @brief Type that proxies to an array.
template <typename T>
class array_proxy {
 public:
    // Types
    typedef std::ptrdiff_t difference_type;
    /// @brief Compare index supported for container.
    static bool
    compare_index(Container& container, index_type a, index_type b) {
        return a < b;
    }
}

Тогда, должно быть хорошо скомпилировать код.