Question

I implemented a boost python converter based on the answer to this question to automagically convert from a Python iterable to a C++ vector. And, by implemented, I mean copied the code verbatim. The converter works like a charm thanks to @Tanner Sansbury.

I use the converter to accept a vector argument to a constructor. However, when the converter is loaded, I get the following error when I call the non-iterable int constructor:

>>> a = term.Term([3])
>>> b = term.Term(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not utterable

Even though both constructors are declared in the boost python module like so:

bp::class_<expr::Term>("Term", bp::init<int>())
    .def(bp::init<std::vector<int> const&>())

If the converter is not loaded, the int constructor works and I get an error when trying to use the vector constructor as expected:

>>> a = term.Term(3)
>>> b = term.Term([3]);
    ArgumentError: Python argument types in
        Term.__init__(Term, list)
    did not match C++ signature:
        __init__(_object*, std::vector<int, std::allocator<int> >)
        __init__(_object*, int)

Is this a feature, bug or am I doing something wrong?

Était-ce utile?

La solution

It looks like a bug to me. As best as I can tell, the dispatch is not properly handling the return from the convertible check. Additionally, I observe different results by changing the order in which constructors are registered (either via boost::python::init or boost::python::make_constructor()).

For example, with a setup similar to the problem described in the question, the following produces similar results:

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

  // ...

  python::class_<spam>("Spam", python::no_init)
    .def(python::init<int>())
    .def(python::init<std::vector<int> >())
    ;
}

Interactive Python:

>>> import example
>>> spam = example.Spam(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>> spam = example.Spam([42])

And upon changing the init order:

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

  // ...

  python::class_<spam>("Spam", python::no_init)
    .def(python::init<std::vector<int> >())
    .def(python::init<int>())
    ;
}

The same Python code works:

>>> import example
>>> spam = example.Spam(42)
>>> spam = example.Spam([42])

Given this unspecified behavior, if the constructors are not functionality different and are only provided as a convenience or to be Pythonic, then it may be worth patching the class constructor in Python to always use the list constructor. For instance, a C++ extension example module would be renamed to _example, and example.py would perform the necessary patching. Here is a complete example:

#include <vector>
#include <boost/python.hpp>
#include <boost/python/stl_iterator.hpp>

/// @brief Type that allows for registration of conversions from
///        python iterable types.
struct iterable_converter
{
  /// @note Registers converter from a python interable type to the
  ///       provided type.
  template <typename Container>
  iterable_converter&
  from_python()
  {
    boost::python::converter::registry::push_back(
      &iterable_converter::convertible,
      &iterable_converter::construct<Container>,
      boost::python::type_id<Container>());
    return *this;
  }

  /// @brief Check if PyObject is iterable.
  static void* convertible(PyObject* object)
  {
    return PyObject_GetIter(object) ? object : NULL;
  }

  /// @brief Convert iterable PyObject to C++ container type.
  ///
  /// Container Concept requirements:
  ///
  ///   * Container::value_type is CopyConstructable.
  ///   * Container can be constructed and populated with two iterators.
  ///     I.e. Container(begin, end)
  template <typename Container>
  static void construct(
    PyObject* object,
    boost::python::converter::rvalue_from_python_stage1_data* data)
  {
    namespace python = boost::python;
    // Object is a borrowed reference, so create a handle indicting it is
    // borrowed for proper reference counting.
    python::handle<> handle(python::borrowed(object));

    // Obtain a handle to the memory block that the converter has allocated
    // for the C++ type.
    typedef python::converter::rvalue_from_python_storage<Container>
                                                                 storage_type;
    void* storage = reinterpret_cast<storage_type*>(data)->storage.bytes;

    typedef python::stl_input_iterator<typename Container::value_type>
                                                                     iterator;

    // Allocate the C++ type into the converter's memory block, and assign
    // its handle to the converter's convertible variable.  The C++
    // container is populated by passing the begin and end iterators of
    // the python object to the container's constructor.
    data->convertible = new (storage) Container(
      iterator(python::object(handle)), // begin
      iterator());                      // end
  }
};

/// @brief Mockup class.
struct spam
{
  explicit spam(const std::vector<int>&) {}
};

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

  // Register interable conversions.
  iterable_converter()
    .from_python<std::vector<int> >()
    ;

  // Expose Spam with a single constructor.  It will be adapted.
  python::class_<spam>("Spam", python::init<std::vector<int> >());
}

example.py that will patch _example.Spam.__init__:

from _example import *

def _patched_spam_init():
    ''' Monkey-patch Spam.__init__ to force the first argument to be
        iterable.

    '''
    # Get handle to delegate.
    _Spam_init = Spam.__init__

    # Patched method that delegates to the original.
    def patch(self, value):
        try:
            (x for x in value)
        except TypeError:
            value = [value]
        return _Spam_init(self, value)

    # Set __init__ to the patched method.
    Spam.__init__ = patch

_patched_spam_init()

The patching occurs transparently to the end user. The same Interactive Python code works:

>>> import example
>>> spam = example.Spam(42)
>>> spam = example.Spam([42])

If the constructors are functionality different, then patching may still be helpful. One could expose helper functions via make_constructor() with a unique tag type used to force the appropriately dispatch in Boost.Python. Then, the extension class could be monkey patched in Python to inspect the arguments and invoke the appropriate class constructor by the tag type. As the tag type is only used internally between the C++ extension and the patching Python module, the dispatching would be transparent to the end user.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top