2016-06-14 9 views
11

Um eine C++ Ausnahme von Python in einer Art und Weise zu machen, die tatsächlich funktioniert, haben Sie so etwas wie schreiben:Boost.Python hinzufügen Bindungen an bestehende PyObject (für die Ausnahmebehandlung)

std::string scope = py::extract<std::string>(py::scope().attr("__name__")); 
std::string full_name = scope + "." + name; 
PyObject* exc_type = PyErr_NewException(&full_name[0], PyExc_RuntimeError, 0); 
// ... 

Aber das doesn‘ In Boost.Python scheinen sie mit allem anderen zu interagieren. Wenn ich machen möchten:

struct Error { int code; }; 

konnte ich schreiben:

py::class_<Error>("Error", py::no_init) 
    .def_readonly("code", &Error::code) 
; 

Wie kann ich die Klasse kombinieren für Error mit Ausnahme Schöpfung auf PyErr_NewException Bindung? Grundsätzlich möchte ich throw Error{42} haben und das funktioniert in der offensichtlichen Weise von Python: Ich kann durch Error oder RuntimeError fangen und habe die Arbeit, und ich kann durch AssertionError (oder ähnliches) fangen und habe weder das Error fangen noch eine SystemError werfen .

+1

etwas off topic Vorschlag aber, [ 'Cython' scheint dies gut zu handhaben] (http://docs.cython.org/src/userguide/wrapping_CPlusPlus.html#exceptions) . Vielleicht könntest du den meisten 'C++' Code mit 'boost-python' verbinden und solche heiklen Fälle mit flexibleren Werkzeugen wie' Cython' behandeln? –

Antwort

5

Der mit class_ erstellte Python-Typ hat ein inkompatibles Layout mit Python exceptions Typen. Der Versuch, einen Typ zu erstellen, der beide in seiner Hierarchie enthält, schlägt mit TypeError fehl. Da der Python außer Klausel Typüberprüfung durchzuführen, ist eine Option ist eine Python Ausnahme zu erstellen, dass Typ:

  • von dem gewünschten Python Ausnahmetyp stammt (en)
  • Proxies zu einem eingebetteten Subjekt-Objekt, das ist eine Instanz eines Typs durch Boost.Python ausgesetzt

Dieser Ansatz erfordert ein paar Schritte:

  • erstellen Python Ausnahmetyp, die sich aus Python Ausnahmen
  • modifizieren die benutzerdefinierten __delattr__ der Python Ausnahme, __getattr__ und __setattr Methoden, so dass sie Proxy zu einer eingebetteten Gegenstand Objekt
  • die benutzerdefinierten Initialisierer der Python Ausnahme Patch ein Subjekt Objekt einzubetten, zu dem es wird Proxy

Eine reine Python-Implementierung des Ansatzes wäre wie folgt:

def as_exception(base): 
    ''' Decorator that will return a type derived from `base` and proxy to the 
     decorated class. 

    ''' 
    def make_exception_type(wrapped_cls): 
     # Generic proxying to subject. 
     def del_subject_attr(self, name): 
      return delattr(self._subject, name) 

     def get_subject_attr(self, name): 
      return getattr(self._subject, name) 

     def set_subject_attr(self, name, value): 
      return setattr(self._subject, name, value) 

     # Create new type that derives from base and proxies to subject. 
     exception_type = type(wrapped_cls.__name__, (base,), { 
      '__delattr__': del_subject_attr, 
      '__getattr__': get_subject_attr, 
      '__setattr__': set_subject_attr, 
     }) 

     # Monkey-patch the initializer now that it has been created. 
     original_init = exception_type.__init__ 

     def init(self, *args, **kwargs): 
      original_init(self, *args, **kwargs) 
      self.__dict__['_subject'] = wrapped_cls(*args, **kwargs) 
     exception_type.__init__ = init 

     return exception_type 
    return make_exception_type 


@as_exception(RuntimeError) 
class Error: 
    def __init__(self, code): 
     self.code = code 

assert(issubclass(Error, RuntimeError)) 
try: 
    raise Error(42) 
except RuntimeError as e: 
    assert(e.code == 42) 
except: 
    assert(False) 

der gleiche allgemeine Ansatz kann verwendet werden, von Boost.Python, die Notwendigkeit, das Äquivalent von class_ für Ausnahmen schreiben zu vermeiden. Es gibt jedoch zusätzliche Schritte und Überlegungen:

  • einen Übersetzer mit boost::python::register_exception_translator() registrieren, die die benutzerdefinierte Python Ausnahme wird konstruieren, wenn eine Instanz des C++ Objekt geworfen wird
  • den Motivtyp kann nicht eine exponierte Initialisierer haben zu Python. Daher sollte beim Erstellen einer Instanz der Ausnahme in Python versucht werden, den Betreff mit __init__ zu initialisieren. Auf der anderen Seite, wenn eine Instanz der Ausnahme in C++ erstellt wird, sollte eine To-Python-Konvertierung verwendet werden, um __init__ zu vermeiden.
  • kann es sinnvoll sein, Python-Konverter zu registrieren, damit eine Instanz des Ausnahmetyps von Python an C++ übergeben und in eine Instanz des umgebrochenen Subjekts konvertiert werden kann.

Unten finden Sie ein vollständiges Beispiel demonstrating die oben beschriebenen Ansatz:

#include <boost/python.hpp> 

namespace exception { 
namespace detail { 

/// @brief Return a Boost.Python object given a borrowed object. 
template <typename T> 
boost::python::object borrowed_object(T* object) 
{ 
    namespace python = boost::python; 
    python::handle<T> handle(python::borrowed(object)); 
    return python::object(handle); 
} 

/// @brief Return a tuple of Boost.Python objects given borrowed objects. 
boost::python::tuple borrowed_objects(
    std::initializer_list<PyObject*> objects) 
{ 
    namespace python = boost::python; 
    python::list objects_; 

    for(auto&& object: objects) 
    { 
    objects_.append(borrowed_object(object)); 
    } 

    return python::tuple(objects_); 
} 

/// @brief Get the class object for a wrapped type that has been exposed 
///  through Boost.Python. 
template <typename T> 
boost::python::object get_instance_class() 
{ 
    namespace python = boost::python; 
    python::type_info type = python::type_id<T>(); 
    const python::converter::registration* registration = 
    python::converter::registry::query(type); 

    // If the class is not registered, return None. 
    if (!registration) return python::object(); 

    return detail::borrowed_object(registration->get_class_object()); 
} 

} // namespace detail 
namespace proxy { 

/// @brief Get the subject object from a proxy. 
boost::python::object get_subject(boost::python::object proxy) 
{ 
    return proxy.attr("__dict__")["_obj"]; 
} 

/// @brief Check if the subject has a subject. 
bool has_subject(boost::python::object proxy) 
{ 
    return boost::python::extract<bool>(
    proxy.attr("__dict__").attr("__contains__")("_obj")); 
} 

/// @brief Set the subject object on a proxy object. 
boost::python::object set_subject(
    boost::python::object proxy, 
    boost::python::object subject) 
{ 
    return proxy.attr("__dict__")["_obj"] = subject; 
} 

/// @brief proxy's __delattr__ that delegates to the subject. 
void del_subject_attr(
    boost::python::object proxy, 
    boost::python::str name) 
{ 
    delattr(get_subject(proxy), name); 
}; 

/// @brief proxy's __getattr__ that delegates to the subject. 
boost::python::object get_subject_attr(
    boost::python::object proxy, 
    boost::python::str name) 
{ 
    return getattr(get_subject(proxy), name); 
}; 

/// @brief proxy's __setattr__ that delegates to the subject. 
void set_subject_attr(
    boost::python::object proxy, 
    boost::python::str name, 
    boost::python::object value) 
{ 
    setattr(get_subject(proxy), name, value); 
}; 

boost::python::dict proxy_attrs() 
{ 
    // By proxying to Boost.Python exposed object, one does not have to 
    // reimplement the entire Boost.Python class_ API for exceptions. 

    // Generic proxying. 
    boost::python::dict attrs; 
    attrs["__detattr__"] = &del_subject_attr; 
    attrs["__getattr__"] = &get_subject_attr; 
    attrs["__setattr__"] = &set_subject_attr; 
    return attrs; 
} 

} // namespace proxy 

/// @brief Registers from-Python converter for an exception type. 
template <typename Subject> 
struct from_python_converter 
{ 
    from_python_converter() 
    { 
    boost::python::converter::registry::push_back(
     &convertible, 
     &construct, 
     boost::python::type_id<Subject>() 
    ); 
    } 

    static void* convertible(PyObject* object) 
    { 
    namespace python = boost::python; 
    python::object subject = proxy::get_subject(
     detail::borrowed_object(object) 
    ); 

    // Locate registration based on the C++ type. 
    python::object subject_instance_class = 
     detail::get_instance_class<Subject>(); 
    if (!subject_instance_class) return nullptr; 

    bool is_instance = (1 == PyObject_IsInstance(
     subject.ptr(), 
     subject_instance_class.ptr() 
    )); 
    return is_instance 
     ? object 
     : nullptr; 
    } 

    static void construct(
    PyObject* object, 
    boost::python::converter::rvalue_from_python_stage1_data* data) 
    { 
    // Object is a borrowed reference, so create a handle indicting it is 
    // borrowed for proper reference counting. 
    namespace python = boost::python; 
    python::object proxy = detail::borrowed_object(object); 

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

    // Copy construct the subject into the converter storage block. 
    python::object subject = proxy::get_subject(proxy); 
    new (storage) Subject(python::extract<const Subject&>(subject)()); 

    // Indicate the object has been constructed into the storage. 
    data->convertible = storage; 
    } 

}; 

/// @brief Expose an exception type in the current scope, that embeds and 
//   proxies to the Wrapped type. 
template <typename Wrapped> 
class exception: 
    boost::python::object 
{ 
public: 

    /// @brief Expose a RuntimeError exception type with the provided name. 
    exception(const char* name) : exception(name, {}) {} 

    /// @brief Expose an expcetion with the provided name, deriving from the 
    ///  borrowed base type. 
    exception(
    const char* name, 
    PyObject* borrowed_base 
) : exception(name, {borrowed_base}) {} 

    /// @brief Expose an expcetion with the provided name, deriving from the 
    ///  multiple borrowed base type. 
    exception(
    const char* name, 
    std::initializer_list<PyObject*> borrowed_bases 
) : exception(name, detail::borrowed_objects(borrowed_bases)) {} 

    /// @brief Expose an expcetion with the provided name, deriving from tuple 
    ///  of bases. 
    exception(
    const char* name, 
    boost::python::tuple bases) 
    { 
    // Default to deriving from Python's RuntimeError. 
    if (!bases) 
    { 
     bases = make_tuple(detail::borrowed_object(PyExc_RuntimeError)); 
    } 

    register_exception_type(name, bases); 
    patch_initializer(); 
    register_translator(); 
    } 

public: 

    exception& enable_from_python() 
    { 
    from_python_converter<Wrapped>{}; 
    return *this; 
    } 

private: 

    /// @brief Handle to this class object. 
    boost::python::object this_class_object() { return *this; } 

    /// @brief Create the Python exception type and install it into this object. 
    void register_exception_type(
    std::string name, 
    boost::python::tuple bases) 
    { 
    // Copy the instance class' name and scope. 
    namespace python = boost::python; 
    auto scoped_name = python::scope().attr("__name__") + "." + name; 

    // Docstring handling. 
    auto docstring = detail::get_instance_class<Wrapped>().attr("__doc__"); 

    // Create exception dervied from the desired exception types, but with 
    // the same name as the Boost.Python class. This is required because 
    // Python exception types and Boost.Python classes have incompatiable 
    // layouts. 
    // >> type_name = type(fullname, (bases,), {proxying attrs}) 
    python::handle<> handle(PyErr_NewExceptionWithDoc(
     python::extract<char*>(scoped_name)(), 
     docstring ? python::extract<char*>(docstring)() : nullptr, 
     bases.ptr(), 
     proxy::proxy_attrs().ptr() 
    )); 

    // Assign the exception type to this object. 
    python::object::operator=(python::object{handle}); 

    // Insert this object into current scope. 
    setattr(python::scope(), name, this_class_object()); 
    } 

    /// @brief Patch the initializer to install the delegate object. 
    void patch_initializer() 
    { 
    namespace python = boost::python; 
    auto original_init = getattr(this_class_object(), "__init__"); 

    // Use raw function so that *args and **kwargs can transparently be 
    // passed to the initializers. 
    this_class_object().attr("__init__") = python::raw_function(
     [original_init](
     python::tuple args, // self + *args 
     python::dict kwargs) // **kwargs 
     { 
     original_init(*args, **kwargs); 
     // If the subject does not exists, then create it. 
     auto self = args[0]; 
     if (!proxy::has_subject(self)) 
     { 
      proxy::set_subject(self, detail::get_instance_class<Wrapped>()(
      *args[python::slice(1, python::_)], // args[1:] 
      **kwargs 
     )); 
     } 

     return python::object{}; // None 
     }); 
    } 

    // @brief Register translator within the Boost.Python exception handling 
    //  chaining. This allows for an instance of the wrapped type to be 
    //  converted to an instance of this exception. 
    void register_translator() 
    { 
    namespace python = boost::python; 
    auto exception_type = this_class_object(); 
    python::register_exception_translator<Wrapped>(
     [exception_type](const Wrapped& proxied_object) 
     { 
     // Create the exception object. If a subject is not installed before 
     // the initialization of the instance, then a subject will attempt to 
     // be installed. As the subject may not be constructible from Python, 
     // manually inject a subject after construction, but before 
     // initialization. 
     python::object exception_object = exception_type.attr("__new__")(
      exception_type 
     ); 

     proxy::set_subject(exception_object, python::object(proxied_object)); 

     // Initialize the object. 
     exception_type.attr("__init__")(exception_object); 

     // Set the exception. 
     PyErr_SetObject(exception_type.ptr(), exception_object.ptr()); 
     }); 
    } 
}; 

// @brief Visitor that will turn the visited class into an exception, 
/// enabling exception translation. 
class export_as_exception 
    : public boost::python::def_visitor<export_as_exception> 
{ 
public: 

    /// @brief Expose a RuntimeError exception type. 
    export_as_exception() : export_as_exception({}) {} 

    /// @brief Expose an expcetion type deriving from the borrowed base type. 
    export_as_exception(PyObject* borrowed_base) 
    : export_as_exception({borrowed_base}) {} 

    /// @brief Expose an expcetion type deriving from multiple borrowed 
    ///  base types. 
    export_as_exception(std::initializer_list<PyObject*> borrowed_bases) 
    : export_as_exception(detail::borrowed_objects(borrowed_bases)) {} 

    /// @brief Expose an expcetion type deriving from multiple bases. 
    export_as_exception(boost::python::tuple bases) : bases_(bases) {} 

private: 

    friend class boost::python::def_visitor_access; 

    template <typename Wrapped, typename ...Args> 
    void visit(boost::python::class_<Wrapped, Args...> instance_class) const 
    { 
    exception<Wrapped>{ 
     boost::python::extract<const char*>(instance_class.attr("__name__"))(), 
     bases_ 
    }; 
    } 

private: 
    boost::python::tuple bases_; 
}; 

} // namespace exception 

struct foo { int code; }; 

struct spam 
{ 
    spam(int code): code(code) {} 
    int code; 
}; 

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

    // Expose `foo` as `example.FooError`. 
    python::class_<foo>("FooError", python::no_init) 
    .def_readonly("code", &foo::code) 
    // Redefine the exposed `example.FooError` class as an exception. 
    .def(exception::export_as_exception(PyExc_RuntimeError)); 
    ; 

    // Expose `spam` as `example.Spam`. 
    python::class_<spam>("Spam", python::init<int>()) 
    .def_readwrite("code", &spam::code) 
    ; 

    // Also expose `spam` as `example.SpamError`. 
    exception::exception<spam>("SpamError", {PyExc_IOError, PyExc_SystemError}) 
    .enable_from_python() 
    ; 

    // Verify from-python. 
    python::def("test_foo", +[](int x){ throw foo{x}; }); 
    // Verify to-Python and from-Python. 
    python::def("test_spam", +[](const spam& error) { throw error; }); 
} 

Im obigen Beispiel ist die C++ foo Typ wie example.FooError ausgesetzt ist, neu definiert example.FooError wird dann auf eine Ausnahmetyp, der stammt von RuntimeError und Proxies zum ursprünglichen example.FooError. Zusätzlich wird der C++ -Typ als example.Spam exponiert, und ein Ausnahmetyp example.SpamError wird definiert, der von IOError und SystemError abgeleitet ist, und Proxies zu example.Spam. Die example.SpamError ist auch in den C++ spam Typ umwandelbar.

Interaktive Nutzung:

>>> import example 
>>> try: 
...  example.test_foo(100) 
... except example.FooError as e: 
...  assert(isinstance(e, RuntimeError)) 
...  assert(e.code == 100) 
... except: 
...  assert(False) 
... 
>>> try: 
...  example.test_foo(101) 
... except RuntimeError as e: 
...  assert(isinstance(e, example.FooError)) 
...  assert(e.code == 101) 
... except: 
...  assert(False) 
... 
... spam_error = example.SpamError(102) 
... assert(isinstance(spam_error, IOError)) 
... assert(isinstance(spam_error, SystemError)) 
>>> try: 
...  example.test_spam(spam_error) 
... except IOError as e: 
...  assert(e.code == 102) 
... except: 
...  assert(False) 
... 
+0

Das ist ziemlich erstaunlich. Also, im Grunde ist eine C++ - Exception-Hierarchie aus, da es wirklich keine Möglichkeit gibt, das in Python über Boost auszudrücken? – Barry