Boost.Python 向现有 PyObject 添加绑定(用于异常处理)

Boost.Python add bindings to existing PyObject (for exception handling)

为了以实际有效的方式向 Python 公开 C++ 异常,您必须编写如下内容:

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);
// ...

但这似乎与 Boost.Python 中的任何其他内容没有交互。如果我要曝光:

struct Error { int code; };

我可以写:

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

如何将 Error 的 class 绑定与 PyErr_NewException 上的异常创建相结合?基本上,我想 throw Error{42} 并以明显的方式从 Python 开始工作:我可以赶上 ErrorRuntimeError 并完成那项工作,我可以赶上AssertionError(或类似的)并且既不捕获 Error 也不抛出 SystemError

Python 类型与 class_ has an incompatible layout with Python exceptions 类型一起创建。尝试创建在其层次结构中同时包含这两种类型的类型将失败并返回 TypeError。由于 Python except 子句将执行类型检查,因此一种选择是创建一个 Python 异常类型:

  • 源自所需的 Python 异常类型
  • 嵌入式主题对象的代理,该对象是通过 Boost.Python
  • 公开的类型的实例

此方法需要几个步骤:

  • 创建一个 Python 异常类型,派生自 Python 异常
  • 修改用户定义的 Python 异常的 __delattr____getattr____setattr 方法,以便它们代理到嵌入的主题对象
  • 修补用户定义的 Python 异常的初始值设定项以嵌入它将代理的主题对象

该方法的纯 Python 实现如下:

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)

Boost.Python 可以使用相同的通用方法,避免为异常编写等同于 class_ 的需要。但是,还有其他步骤和注意事项:

  • boost::python::register_exception_translator() 注册一个转换器,它将在抛出 C++ 对象的实例时构造用户定义的 Python 异常
  • 主题类型可能没有暴露给 Python 的初始值设定项。因此,在 Python 中创建异常的实例时,应尝试使用 __init__ 初始化主题。另一方面,在 C++ 中创建异常实例时,应该使用 to-python 转换以避免 __init__.
  • 可能希望注册 from-Python 转换器以允许将异常类型的实例从 Python 传递到 C++,将其转换为包装主题的实例。

下面是一个完整的例子demonstrating上面描述的方法:

#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; });
}

在上面的示例中,C++ foo 类型公开为 example.FooError,然后 example.FooError 被重新定义为派生自 RuntimeError 的异常类型并代理到原来example.FooError。此外,C++ spam 类型公开为 example.Spam,并且定义了派生自 IOErrorSystemError 的异常类型 example.SpamError,以及 example.Spamexample.SpamError 也可以转换为 C++ spam 类型。

交互使用:

>>> 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)
...