I am using boost.python to wrap a C++ class 'A' which takes a string as constructor. I then have a function 'fun(A& arg)' which takes a reference to an 'A' as parameter. I would like to have a python wrapper for 'fun' which is such that if I pass a variable which is a reference to a python string, this reference is first automatically converted to a reference to an 'A'.
An example might help. On the python side, I would like to be able to do this:
a = 'some string'
fun(a)
and then have 'a' actually be (a reference to) an 'A', not (a reference to) the original string. I want to do this because I would like to be able to avoid writing this as
a = A('some string')
fun(a)
(you may have good reasons to doubt that this is a relevant saver, but let's just assume that it matters for me).
Is anything like this possible? If not using boost.python, perhaps directly using the Python-C API?
Note: I am aware of the fact that if I would write
fun('some string')
there is no way for the reference to the string to be converted to be a reference to some other type.
This is possible, but the solution may be dependent on the Python implementation.
For example, in Python 2.7, the inspect
module and sys.settrace()
can be used to modify locals()
on a specific frame. This can also be accomplished in the Python/C API, but it is often far more manageable to manipulate Python frames within Python.
In the below example.py
, the update_locals()
function will update the locals()
in a given frame:
import inspect
import sys
def _force_locals(old_frame, new_locals):
''' Force a new set of locals on a given frame.
:param old_frame: The frame to which locals will be applied.
:type old_frame: frame.
:param new_locals: What locals() should return for old_frame.
:type new_locals: dict.
.. note:: This function will force a custom trace function onto
the old_frame. Within the context of a trace function
(often used for debugging), a frame's f_locals is
modifiable. In most execution paths, f_locals is
writable but not modifiable.
'''
# Force tracing by setting the global tracing function to
# any non-None function.
if not sys.gettrace():
sys.settrace(lambda *args, **keys: None)
# Custom trace function that will force locals.
def trace(frame, event, arg):
# Update the frame's locals.
frame.f_locals.update(new_locals)
# Set frame to use default trace, preventing this trace
# function from resetting the locals on each trace call.
del frame.f_trace
# Set the old frame's trace function to the custom trace.
old_frame.f_trace = trace
def update_locals(frame, *refs):
''' Modifies a frame's locals based on the provided references.
:param frame: The frame from which a locals will be updated.
:type frame: frame.
:param refs: Iterable pair of (old_ref, new_ref) tuples.
:type refs: Iterable type of pairs.
'''
new_locals = frame.f_locals.copy()
has_changes = False
# If a frame's local has an identity patch with a provided
# reference, then update new_locals with the new reference.
for key, ref in new_locals.iteritems():
for old_ref, new_ref in refs:
if ref is old_ref:
new_locals[key] = new_ref
has_changes = True
# If new_locals has changes, then force them onto the frame.
if has_changes:
_force_locals(frame, new_locals)
Interactive usage:
>>> import example
>>> import inspect
>>> x = 42
>>> x
42
>>> example.update_locals(inspect.currentframe(), (x, '3.14'))
>>> x
'3.14'
The x
variable referenced the int(42)
object, but the example.update_locals()
function changed x
to reference str('3.14')
object.
With being able to modify the caller's frame, the next step is to monkey patch the C++ fun()
in Python to construct an instance of A
if the argument is an instance of str
, then delegate to the C++ fun(A&)
function and update the caller's frame.
In example below, a C++ Spam
type and fun(Spam&)
function are exposed to the _example
Python module.
#include <iostream>
#include <string>
#include <boost/python.hpp>
/// @brief Mockup type.
class Spam
{
public:
explicit Spam(std::string str)
: str_(str)
{}
void action()
{
std::cout << "Spam::action(): " << str_ << std::endl;
}
private:
std::string str_;
};
/// @brief Mockup function.
void fun(Spam& spam)
{
std::cout << "fun() -> ";
spam.action();
}
BOOST_PYTHON_MODULE(_example)
{
namespace python = boost::python;
python::class_<Spam>("Spam", python::init<std::string>());
python::def("fun", &fun);
}
A higher level example
module will monkey patch _example.fun()
to construct a Spam
object if the argument provided to fun()
is an instance of str
and manipulate the caller's frame in a similar manner as demonstrated above:
from _example import *
import inspect
import sys
def _force_locals(old_frame, new_locals):
''' Force a new set of locals on a given frame.
:param old_frame: The frame to which locals will be applied.
:type old_frame: frame.
:param new_locals: What locals() should return for old_frame.
:type new_locals: dict.
.. note:: This function will force a custom trace function onto
the old_frame. Within the context of a trace function
(often used for debugging), a frame's f_locals is
modifiable. In most execution paths, f_locals is
writable but not modifiable.
'''
# Force tracing by setting the global tracing function to
# any non-None function.
if not sys.gettrace():
sys.settrace(lambda *args, **keys: None)
# Custom trace function that will force locals.
def trace(frame, event, arg):
# Update the frame's locals.
frame.f_locals.update(new_locals)
# Set frame to use default trace, preventing this trace
# function from resetting the locals on each trace call.
del frame.f_trace
# Set the old frame's trace function to the custom trace.
old_frame.f_trace = trace
def _update_locals(frame, *refs):
''' Modifies a frame's locals based on the provided references.
:param frame: The frame from which a locals will be updated.
:type frame: frame.
:param refs: Iterable pair of (old_ref, new_ref) tuples.
:type refs: Iterable type of pairs.
'''
new_locals = frame.f_locals.copy()
has_changes = False
# If a frame's local has an identity patch with a provided
# reference, then update new_locals with the new reference.
for key, ref in new_locals.iteritems():
for old_ref, new_ref in refs:
if ref is old_ref:
new_locals[key] = new_ref
has_changes = True
# If new_locals has changes, then force them onto the frame.
if has_changes:
_force_locals(frame, new_locals)
def _patch_fun():
old_fun = fun
# Create a function that will perform custom operations then
# delegate to the original function.
def patch(spam, *args, **kwargs):
if isinstance(spam, str):
old_spam, spam = spam, Spam(spam)
# In the caller's frame, force the variables that reference
# old_spam to now reference spam.
_update_locals(
inspect.currentframe(1), # Caller's frame.
(old_spam, spam))
return old_fun(spam, *args, **kwargs)
return patch
fun = _patch_fun()
Interactive usage:
>>> import example
>>> s1 = example.Spam('abc')
>>> type(s1)
<class '_example.Spam'>
>>> example.fun(s1)
fun() -> Spam::action(): abc
>>> type(s1) # s1's type has not changed.
<class '_example.Spam'>
>>> s2 = 'def'
>>> type(s2)
<type 'str'>
>>> example.fun(s2)
fun() -> Spam::action(): def
>>> type(s2) # s2's type has changed.
<class '_example.Spam'>
>>> example.fun('ghi')
fun() -> Spam::action(): ghi
Note that the frame manipulation in the above example only modifies's fun()
's caller's frame and not the entire stack.
For that to work fun(a)
would have to modify the original a
object reference. What fun(a)
actually gets is a local copy of object reference a
, not the original a
object reference passed as an argument.
In other words, Python does not work like that, you would need to call it as a = fun(a)
to be able to change reference a
(not the object it refers to).