Question

I am using boost.python to make two C++ classes available to Python,

class X {
   public:
      X();
}
class Y {
   ...
}

BOOST_PYTHON_MODULE(test) {
    class_<X>("X", init<>());
    class_<Y>("Y", init<>());
}

Whenever I create a new X in Python, I would like to run some code in C++ which creates a local object 'y' of type Y. So effectively, when I do

x = X()

in Python, I want this to run

y = Y()

as well, but from the X::X() constructor in C++.

I thought to use something like

scope().attr("y")=...

inside the X::X() constructor, but scope is always returning a NoneType object when called in this way (it works fine if I use this construction from within BOOST_PYTHON_MODULE, but that's not the right place for me).

Was it helpful?

Solution

boost::python::scope is more akin to namespaces rather than the scope of a code block. The Python/C API exposes a dictionary similar to locals() through the PyEval_GetLocals() function. It is possible to use this dictionary to inject variables into the current scope.

// Borrow a reference from the locals dictionary to create a handle.
// If PyEval_GetLocals() returns NULL, then Boost.Python will throw.
namespace python = boost::python;
python::object locals(python::borrowed(PyEval_GetLocals()));

// Inject an instance of Y into the frame's locals as variable 'y'.
// Boost.Python will handle the conversion of C++ Y to Python Y.
locals["y"] = Y();

Here is a complete example where an instance of example.Y is injected into the caller's scope as variable y when example.X is constructed.

#include <boost/python.hpp>

/// @brief Mockup types.
struct X {};
struct Y {};

/// @brief Auxiliary function that will create X and inject an Y object
///        as 'y' into the caller's frame.
X* make_x_and_inject_y()
{
  // Boost.Python objects may throw, so use a smart pointer that can
  // release ownership to manage memory.
  std::auto_ptr<X> x(new X());

  // Borrow a reference from the locals dictionary to create a handle.
  // If PyEval_GetLocals() returns NULL, then Boost.Python will throw.
  namespace python = boost::python;
  python::object locals(python::borrowed(PyEval_GetLocals()));

  // Inject an instance of Y into the frame's locals as variable 'y'.
  // Boost.Python will handle the conversion of C++ Y to Python Y.
  locals["y"] = Y();

  // Transfer ownership of X to Boost.Python.
  return x.release();
}

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

  // Expose X, explicitly suppressing Boost.Python from creating a
  // default constructor, and instead exposing a custom constructor.
  python::class_<X>("X", python::no_init)
    .def("__init__", python::make_constructor(&make_x_and_inject_y))
    ;
  python::class_<Y>("Y", python::init<>());
}

Interactive usage:

>>> import example
>>> def fun():
...     assert('y' not in dir())
...     example.X()
...     assert('y' in dir()) # creating X injects y into scope
... 
>>> assert('y' not in dir())
>>> fun()
>>> assert('y' not in dir())
>>> example.X()
<example.X object at 0xb746fa7c>
>>> assert('y' in dir()) # creating X injects y into scope
>>> assert(isinstance(y, example.Y))

In this implementation, I have opted to expose an auxiliary factory function to Python as X's constructor rather than having X's C++ constructor perform the injection of Y. It is just a personal preference, but I often find that it provides a cleaner delimitation between the languages by limiting the amount of C++ types that are aware of Python.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top