boost::python 导出自定义异常

25 投票
6 回答
9767 浏览
提问于 2025-04-15 19:16

我现在正在用Boost.Python为Python写一个C++扩展。在这个扩展里,有一个函数可能会产生一个异常,这个异常包含了关于错误的信息,不仅仅是一个简单的字符串来描述发生了什么。我希望能把这个异常传递到Python中,这样我就可以捕获它并处理额外的信息。

举个例子:

import my_cpp_module
try:
    my_cpp_module.my_cpp_function()
except my_cpp_module.MyCPPException, e:
    print e.my_extra_data

可惜的是,Boost.Python似乎会把所有的C++异常(那些是std::exception的子类)都转化为RuntimeError。我知道Boost.Python允许实现自定义的异常转换,但是需要使用PyErr_SetObject,这个函数需要两个参数,一个是PyObject*(表示异常的类型),另一个是PyObject*(表示异常的值)——而我不知道如何从我的Boost.Python类中获取这两个东西。也许有我还没找到的方法(那就太好了)。否则,有人知道怎么把自定义的C++异常导出到Python,以便我能在Python中捕获它吗?

6 个回答

1

多亏了可变参数模板和通用的 lambda 捕获,我们可以把 Jack Edmond 的回答 简化成更容易管理的形式,并把一些复杂的东西隐藏起来,让用户更轻松使用:

template <class E, class... Policies, class... Args>
py::class_<E, Policies...> exception_(Args&&... args) {
    py::class_<E, Policies...> cls(std::forward<Args>(args)...);
    py::register_exception_translator<E>([ptr=cls.ptr()](E const& e){
        PyErr_SetObject(ptr, py::object(e).ptr());
    });
    return cls;
}

要把 MyCPPException 作为异常暴露出来,你只需要把绑定中的 py::class_ 改成 exception_

exception_<MyCPPException>("MyCPPException", py::init<std::string, std::string>())
    .add_property("message", &MyCPPException::getMessage)
    .add_property("extra_data", &MyCPPException::getExtraData)
;

现在我们又回到了 Boost.Python 的优雅之处:不需要给 class_ 实例命名,不需要额外的 PyObject*,也不需要在其他地方再写一个额外的函数。

4

Jack Edmonds给出的答案定义了一个Python的“异常”类,这个类并没有继承Exception(或者其他任何内置的Python异常类)。所以,虽然可以用下面的方式捕获它:

except my_cpp_extension.MyCPPException as e:
    ...

但是它不能用通常的捕获方式来捕获:

except Exception as e:
    ...

这里是如何创建一个自定义的Python异常类,它是继承Exception的。

29

解决方案是像创建普通的C++类一样来创建你的异常类。

class MyCPPException : public std::exception {...}

关键在于,所有的boost::python::class_实例都持有一个对象类型的引用,这个引用可以通过它们的ptr()函数访问。你可以在用boost::python注册类的时候获取这个引用,方法如下:

class_<MyCPPException> myCPPExceptionClass("MyCPPException"...);
PyObject *myCPPExceptionType=myCPPExceptionClass.ptr();
register_exception_translator<MyCPPException>(&translateFunc);

最后,当你把C++的异常转换成Python的异常时,可以这样做:

void translate(MyCPPException const &e)
{
    PyErr_SetObject(myCPPExceptionType, boost::python::object(e).ptr());
}

这里是一个完整的工作示例:

#include <boost/python.hpp>
#include <assert.h>
#include <iostream>

class MyCPPException : public std::exception
{
private:
  std::string message;
  std::string extraData;
public:
  MyCPPException(std::string message, std::string extraData)
  {
    this->message = message;
    this->extraData = extraData;
  }
  const char *what() const throw()
  {
    return this->message.c_str();
  }
  ~MyCPPException() throw()
  {
  }
  std::string getMessage()
  {
    return this->message;
  }
  std::string getExtraData()
  {
    return this->extraData;
  }
};

void my_cpp_function(bool throwException)
{
  std::cout << "Called a C++ function." << std::endl;
  if (throwException)
    {
      throw MyCPPException("Throwing an exception as requested.",
               "This is the extra data.");
    }
}

PyObject *myCPPExceptionType = NULL;

void translateMyCPPException(MyCPPException const &e)
{
  assert(myCPPExceptionType != NULL);
  boost::python::object pythonExceptionInstance(e);
  PyErr_SetObject(myCPPExceptionType, pythonExceptionInstance.ptr());
}

BOOST_PYTHON_MODULE(my_cpp_extension)
{
  boost::python::class_<MyCPPException>
    myCPPExceptionClass("MyCPPException",
            boost::python::init<std::string, std::string>());
  myCPPExceptionClass.add_property("message", &MyCPPException::getMessage)
    .add_property("extra_data", &MyCPPException::getExtraData);
  myCPPExceptionType = myCPPExceptionClass.ptr();
  boost::python::register_exception_translator<MyCPPException>
    (&translateMyCPPException);
  boost::python::def("my_cpp_function", &my_cpp_function);
}

下面是调用这个扩展的Python代码:

import my_cpp_extension
try:
    my_cpp_extension.my_cpp_function(False)
    print 'This line should be reached as no exception should be thrown.'
except my_cpp_extension.MyCPPException, e:
    print 'Message:', e.message
    print 'Extra data:',e.extra_data

try:
    my_cpp_extension.my_cpp_function(True)
    print ('This line should not be reached as an exception should have been' +
       'thrown by now.')
except my_cpp_extension.MyCPPException, e:
    print 'Message:', e.message
    print 'Extra data:',e.extra_data

撰写回答