2

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

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

And funnily enough I had tried that and concluded that it did not work... Turned out to be a totally unrelated bug. Thanks for convincing me to stare at my code until I found it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.