Pregunta

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.

¿Fue útil?

Solución

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.

Otros consejos

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top