Il tipo Python creato con class_
ha un layout incompatibile con i tipi Python exceptions
. Il tentativo di creare un tipo contenente entrambi nella sua gerarchia fallirà con un TypeError
. Come il python tranne clausola eseguirà la verifica del tipo, una possibilità è quella di creare digitare un'eccezione Python che:
- deriva dal tipo di eccezione Python desiderato (s)
- proxy per un oggetto soggetto incorporato che è un'istanza di un tipo esposto attraverso Boost.Python
Questo approccio richiede pochi passi:
- creare un pitone tipo di eccezione, derivanti da eccezioni Python
- modificare l'eccezione Python definito dall'utente
__delattr__
, __getattr__
e __setattr
metodi in modo da proxy per un oggetto soggetto incorporato
- rattoppare inizializzatore l'eccezione Python definita dall'utente per incorporare un oggetto soggetto a cui lo farà delega
Un'implementazione Python puro dell'approccio sarebbe la seguente:
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)
lo stesso approccio generale può essere utilizzato da Boost.Python, ovviando alla necessità di scrivere l'equivalente di class_
per le eccezioni. Tuttavia, ci sono passi e considerazioni aggiuntive:
- registrare un traduttore con
boost::python::register_exception_translator()
che costruirà l'eccezione Python definito dall'utente quando un'istanza di C++ oggetto viene generata
- il tipo di soggetto non può avere un inizializzatore esposta a Python. Quindi, quando si crea un'istanza dell'eccezione in Python, si dovrebbe tentare di inizializzare l'oggetto con
__init__
. D'altra parte, quando si crea un'istanza dell'eccezione in C++, si dovrebbe usare una conversione to-python per evitare __init__
.
- Si potrebbe desiderare di registrare da convertitori Python per consentire un'istanza del tipo di eccezione da passare da Python a C++, convertendola in un'istanza dell'oggetto spostato.
Ecco un esempio completo demonstrating l'approccio sopra descritto:
#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; });
}
Nell'esempio precedente, il tipo C++ foo
è esposto come example.FooError
, quindi example.FooError
ottiene ridefinito a un tipo di eccezione che deriva da RuntimeError
e proxy all'originale example.FooError
. Inoltre, il tipo C++ spam
è esposto come example.Spam
e viene definito un tipo di eccezione example.SpamError
che deriva da IOError
e SystemError
e proxy a example.Spam
. Lo example.SpamError
è anche convertibile in tipo C++ spam
.
utilizzo interattivo:
>>> 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)
...
Leggermente fuori tema suggerimento, ma, [ 'Cython' sembra gestire questo bene] (http://docs.cython.org/src/userguide/wrapping_CPlusPlus.html#exceptions) . Forse potresti racchiudere la maggior parte del codice 'C++' con 'boost-python' e gestire casi così complicati con strumenti più flessibili come' Cython'? –