python c++ boost-python

Envolviendo un std:: vector usando boost:: python vector_indexing_suite



c++ boost-python (2)

Debido a las diferencias semánticas entre los idiomas, a menudo es muy difícil aplicar una única solución reutilizable a todos los escenarios cuando se trata de colecciones. El mayor problema es que si bien las colecciones de Python son compatibles directamente con las referencias, las colecciones de C ++ requieren un nivel de shared_ptr indirecto, por ejemplo, al tener tipos de elementos shared_ptr . Sin esta indirección, las colecciones de C ++ no podrán admitir la misma funcionalidad que las colecciones de Python. Por ejemplo, considere dos índices que se refieren al mismo objeto:

s = Spam() spams = [] spams.append(s) spams.append(s)

Sin tipos de elementos tipo puntero, una colección de C ++ no podría tener dos índices que se refieran al mismo objeto. Sin embargo, dependiendo del uso y las necesidades, puede haber opciones que permitan una interfaz Pythonic-ish para los usuarios de Python mientras se mantiene una implementación única para C ++.

  • La solución más Pythonic sería usar un convertidor personalizado que convierta un objeto iterable de Python en una colección de C ++. Vea this respuesta para detalles de implementación. Considere esta opción si:
    • Los elementos de la colección son baratos para copiar.
    • Las funciones de C ++ operan solo en tipos de valores r (es decir, std::vector<> o const std::vector<>& ). Esta limitación evita que C ++ realice cambios en la colección de Python o sus elementos.
  • Mejore vector_indexing_suite capacidades de vector_indexing_suite , reutilizando tantas capacidades como sea posible, como sus proxies para el manejo seguro de la eliminación de índices y la reasignación de la colección subyacente:
    • Exponga el modelo con un HeldType personalizado que funciona como un puntero inteligente y delegue a la instancia o al elemento proxy objeto devuelto desde vector_indexing_suite .
    • Monkey aplica un parche a los métodos de la colección que insertan elementos en la colección para que el HeldType personalizado se establezca para delegar en un elemento proxy.

Al exponer una clase a Boost.Python, el HeldType es el tipo de objeto que se incrusta dentro de un objeto Boost.Python. Al acceder al objeto de tipos envueltos, Boost.Python invoca get_pointer() para el HeldType . La object_holder clase de object_holder proporciona la capacidad de devolver un identificador a una instancia que posee o a un elemento proxy:

/// @brief smart pointer type that will delegate to a python /// object if one is set. template <typename T> class object_holder { public: typedef T element_type; object_holder(element_type* ptr) : ptr_(ptr), object_() {} element_type* get() const { if (!object_.is_none()) { return boost::python::extract<element_type*>(object_)(); } return ptr_ ? ptr_.get() : NULL; } void reset(boost::python::object object) { // Verify the object holds the expected element. boost::python::extract<element_type*> extractor(object_); if (!extractor.check()) return; object_ = object; ptr_.reset(); } private: boost::shared_ptr<element_type> ptr_; boost::python::object object_; }; /// @brief Helper function used to extract the pointed to object from /// an object_holder. Boost.Python will use this through ADL. template <typename T> T* get_pointer(const object_holder<T>& holder) { return holder.get(); }

Con la indirección admitida, lo único que queda es parchear la colección para establecer el object_holder . Una forma limpia y reutilizable de admitir esto es usar def_visitor . Esta es una interfaz genérica que permite que los objetos class_ se extiendan de forma no intrusiva. Por ejemplo, el vector_indexing_suite utiliza esta capacidad.

La clase custom_vector_indexing_suite debajo de monkey parchea el método append() para delegar al método original, y luego invoca a object_holder.reset() con un proxy para el nuevo elemento establecido. Esto hace que el object_holder refiera al elemento contenido dentro de la colección.

/// @brief Indexing suite that will resets the element''s HeldType to /// that of the proxy during element insertion. template <typename Container, typename HeldType> class custom_vector_indexing_suite : public boost::python::def_visitor< custom_vector_indexing_suite<Container, HeldType>> { private: friend class boost::python::def_visitor_access; template <typename ClassT> void visit(ClassT& cls) const { // Define vector indexing support. cls.def(boost::python::vector_indexing_suite<Container>()); // Monkey patch element setters with custom functions that // delegate to the original implementation then obtain a // handle to the proxy. cls .def("append", make_append_wrapper(cls.attr("append"))) // repeat for __setitem__ (slice and non-slice) and extend ; } /// @brief Returned a patched ''append'' function. static boost::python::object make_append_wrapper( boost::python::object original_fn) { namespace python = boost::python; return python::make_function([original_fn]( python::object self, HeldType& value) { // Copy into the collection. original_fn(self, value.get()); // Reset handle to delegate to a proxy for the newly copied element. value.reset(self[-1]); }, // Call policies. python::default_call_policies(), // Describe the signature. boost::mpl::vector< void, // return python::object, // self (collection) HeldType>() // value ); } };

El envoltorio debe ocurrir en tiempo de ejecución y los objetos de functor personalizados no se pueden definir directamente en la clase a través de def() , por lo que se debe usar la función make_function() . Para los funtores, requiere tanto CallPolicies como una secuencia extensible por el frente de MPL que representa la firma.

Aquí hay un ejemplo completo que demonstrates uso del object_holder para delegar a proxies y custom_vector_indexing_suite para parchear la colección.

#include <boost/python.hpp> #include <boost/python/suite/indexing/vector_indexing_suite.hpp> /// @brief Mockup type. struct spam { int val; spam(int val) : val(val) {} bool operator==(const spam& rhs) { return val == rhs.val; } }; /// @brief Mockup function that operations on a collection of spam instances. void modify_spams(std::vector<spam>& spams) { for (auto& spam : spams) spam.val *= 2; } /// @brief smart pointer type that will delegate to a python /// object if one is set. template <typename T> class object_holder { public: typedef T element_type; object_holder(element_type* ptr) : ptr_(ptr), object_() {} element_type* get() const { if (!object_.is_none()) { return boost::python::extract<element_type*>(object_)(); } return ptr_ ? ptr_.get() : NULL; } void reset(boost::python::object object) { // Verify the object holds the expected element. boost::python::extract<element_type*> extractor(object_); if (!extractor.check()) return; object_ = object; ptr_.reset(); } private: boost::shared_ptr<element_type> ptr_; boost::python::object object_; }; /// @brief Helper function used to extract the pointed to object from /// an object_holder. Boost.Python will use this through ADL. template <typename T> T* get_pointer(const object_holder<T>& holder) { return holder.get(); } /// @brief Indexing suite that will resets the element''s HeldType to /// that of the proxy during element insertion. template <typename Container, typename HeldType> class custom_vector_indexing_suite : public boost::python::def_visitor< custom_vector_indexing_suite<Container, HeldType>> { private: friend class boost::python::def_visitor_access; template <typename ClassT> void visit(ClassT& cls) const { // Define vector indexing support. cls.def(boost::python::vector_indexing_suite<Container>()); // Monkey patch element setters with custom functions that // delegate to the original implementation then obtain a // handle to the proxy. cls .def("append", make_append_wrapper(cls.attr("append"))) // repeat for __setitem__ (slice and non-slice) and extend ; } /// @brief Returned a patched ''append'' function. static boost::python::object make_append_wrapper( boost::python::object original_fn) { namespace python = boost::python; return python::make_function([original_fn]( python::object self, HeldType& value) { // Copy into the collection. original_fn(self, value.get()); // Reset handle to delegate to a proxy for the newly copied element. value.reset(self[-1]); }, // Call policies. python::default_call_policies(), // Describe the signature. boost::mpl::vector< void, // return python::object, // self (collection) HeldType>() // value ); } // .. make_setitem_wrapper // .. make_extend_wrapper }; BOOST_PYTHON_MODULE(example) { namespace python = boost::python; // Expose spam. Use a custom holder to allow for transparent delegation // to different instances. python::class_<spam, object_holder<spam>>("Spam", python::init<int>()) .def_readwrite("val", &spam::val) ; // Expose a vector of spam. python::class_<std::vector<spam>>("SpamVector") .def(custom_vector_indexing_suite< std::vector<spam>, object_holder<spam>>()) ; python::def("modify_spams", &modify_spams); }

Uso interactivo:

>>> import example >>> spam = example.Spam(5) >>> spams = example.SpamVector() >>> spams.append(spam) >>> assert(spams[0].val == 5) >>> spam.val = 21 >>> assert(spams[0].val == 21) >>> example.modify_spams(spams) >>> assert(spam.val == 42) >>> spams.append(spam) >>> spam.val = 100 >>> assert(spams[1].val == 100) >>> assert(spams[0].val == 42) # The container does not provide indirection.

Como el vector_indexing_suite aún se está utilizando, el contenedor de C ++ subyacente solo debe modificarse utilizando la API del objeto Python. Por ejemplo, invocar push_back en el contenedor puede causar una reasignación de la memoria subyacente y causar problemas con los proxies Boost.Python existentes. Por otro lado, uno puede modificar los elementos de forma segura, como se hizo a través de la función modify_spams() anterior.

Estoy trabajando en una biblioteca de C ++ con enlaces de Python (usando boost :: python) que representan datos almacenados en un archivo. La mayoría de mis usuarios semi-técnicos usarán Python para interactuar con él, así que necesito hacerlo lo más Pythonic posible. Sin embargo, también tendré programadores de C ++ utilizando la API, por lo que no quiero comprometerme en el lado de C ++ para acomodar los enlaces de Python.

Una gran parte de la biblioteca estará hecha de contenedores. Para hacer las cosas intuitivas para los usuarios de python, me gustaría que se comportaran como las listas de python, es decir:

# an example compound class class Foo: def __init__( self, _val ): self.val = _val # add it to a list foo = Foo(0.0) vect = [] vect.append(foo) # change the value of the *original* instance foo.val = 666.0 # which also changes the instance inside the container print vect[0].val # outputs 666.0

La configuración de prueba

#include <boost/python.hpp> #include <boost/python/suite/indexing/vector_indexing_suite.hpp> #include <boost/python/register_ptr_to_python.hpp> #include <boost/shared_ptr.hpp> struct Foo { double val; Foo(double a) : val(a) {} bool operator == (const Foo& f) const { return val == f.val; } }; /* insert the test module wrapping code here */ int main() { Py_Initialize(); inittest(); boost::python::object globals = boost::python::import("__main__").attr("__dict__"); boost::python::exec( "import test/n" "foo = test.Foo(0.0)/n" // make a new Foo instance "vect = test.FooVector()/n" // make a new vector of Foos "vect.append(foo)/n" // add the instance to the vector "foo.val = 666.0/n" // assign a new value to the instance // which should change the value in vector "print ''Foo ='', foo.val/n" // and print the results "print ''vector[0] ='', vect[0].val/n", globals, globals ); return 0; }

El camino de la shared_ptr

Usando shared_ptr, puedo obtener el mismo comportamiento que anteriormente, pero también significa que tengo que representar todos los datos en C ++ usando punteros compartidos, lo cual no es bueno desde muchos puntos de vista.

BOOST_PYTHON_MODULE( test ) { // wrap Foo boost::python::class_< Foo, boost::shared_ptr<Foo> >("Foo", boost::python::init<double>()) .def_readwrite("val", &Foo::val); // wrap vector of shared_ptr Foos boost::python::class_< std::vector < boost::shared_ptr<Foo> > >("FooVector") .def(boost::python::vector_indexing_suite<std::vector< boost::shared_ptr<Foo> >, true >()); }

En mi configuración de prueba, esto produce el mismo resultado que Python puro:

Foo = 666.0 vector[0] = 666.0

El camino del vector<Foo>

Usar un vector directamente da una buena configuración limpia en el lado C ++. Sin embargo, el resultado no se comporta de la misma manera que Python puro.

BOOST_PYTHON_MODULE( test ) { // wrap Foo boost::python::class_< Foo >("Foo", boost::python::init<double>()) .def_readwrite("val", &Foo::val); // wrap vector of Foos boost::python::class_< std::vector < Foo > >("FooVector") .def(boost::python::vector_indexing_suite<std::vector< Foo > >()); }

Esto produce:

Foo = 666.0 vector[0] = 0.0

Que es "incorrecto": cambiar la instancia original no cambió el valor dentro del contenedor.

Espero no querer demasiado

Curiosamente, este código funciona sin importar cuál de las dos encapsulaciones que uso:

footwo = vect[0] footwo.val = 555.0 print vect[0].val

Lo que significa que boost :: python puede lidiar con la "propiedad compartida falsa" (a través de su mecanismo de devolución by_proxy ). ¿Hay alguna manera de lograr lo mismo al insertar nuevos elementos?

Sin embargo, si la respuesta es no, me encantaría escuchar otras sugerencias: ¿hay un ejemplo en el kit de herramientas de Python donde se implementa una encapsulación de colección similar, pero que no se comporta como una lista de python?

Muchas gracias por leer hasta aquí :)


Desafortunadamente, la respuesta es no, no puedes hacer lo que quieres. En Python, todo es un puntero, y las listas son un contenedor de punteros. El vector C ++ de punteros compartidos funciona porque la estructura de datos subyacente es más o menos equivalente a una lista de python. Lo que está solicitando es hacer que el vector C ++ de la memoria asignada actúe como un vector de punteros, que no se puede hacer.

Veamos qué sucede en las listas de python, con pseudocódigo equivalente a C ++:

foo = Foo(0.0) # Foo* foo = new Foo(0.0) vect = [] # std::vector<Foo*> vect vect.append(foo) # vect.push_back(foo)

En este punto, foo y vect[0] apuntan a la misma memoria asignada, por lo que cambiar *foo cambia *vect[0] .

Ahora con la versión vector<Foo> :

foo = Foo(0.0) # Foo* foo = new Foo(0.0) vect = FooVector() # std::vector<Foo> vect vect.append(foo) # vect.push_back(*foo)

Aquí, vect[0] tiene su propia memoria asignada, y es una copia de * foo. Fundamentalmente, no puede hacer que vect [0] sea la misma memoria que * foo.

En una nota al margen, tenga cuidado con la administración de por vida de footwo cuando use std::vector<Foo> :

footwo = vect[0] # Foo* footwo = &vect[0]

Un anexo posterior puede requerir mover el almacenamiento asignado para el vector, y puede invalidar footwo (& vect [0] puede cambiar).