Question

This seems like something that might not be possible, but I'm trying to implement something like this:

a = 0
with before():
  a += 1

do_thing(a) # does thing with a, whose value is now 1
do_thing(a) # does thing with a, whose value is now 2

So I want something that can take the block within that with statement, save that block somewhere and have it be called prior to each do_thing function call, while using that scope.

Another option is something like this:

@before
def callback():
  a += 1

Rather than a with statement. Either option, I suppose, is fine with me, although the with statement is preferred.

There is something that is supposed to do what I want, I think, here, but I get an error when I actually try it.

Was it helpful?

Solution

You can create a decorator that stores functions in a list, to be attached to another function by another decorator. That seems to be what you think is the hard part of your problem, but it's trivial:

before_funcs = []
def before(func):
    before_funcs.append(func)
    return func

def attach_befores(func):
    @functools.wraps(func)
    def newfunc(*args, **kwargs):
        for before_func in before_funcs:
            before_func()
        return func(*args, **kwargs)
    return newfunc

So, now you can do this:

a = 0

@before
def callback():
    global a
    a += 1

@before
def another():
    global a
    a *= 2

@attach_befores
def do_thing(i):
    print(i)

Notice that you need the global a there, because the function isn't valid otherwise.


And now, you can call it:

do_thing(a)
do_thing(a)
do_thing(a)
do_thing(a)

However, it's not going to give you the result you wanted—in particular, changing the global a will not change the argument passed to the real do_thing function. Why? Because function arguments are evaluated before the function is called. So, rebinding a after the argument has already been evaluated does you no good. It will, of course, still change the argument passed to the next call. So, the output will be:

0
2
6
14

If you just want to modify the argument passed to the function, you don't need all this mucking about with globals. Just have the before function modify the argument, and have the decorator-applier pass the arguments through each before function before passing them to the real function.

Or, alternatively, if you want to modify the globals used by the function, have the function actually use those globals instead of taking parameters.

Or, alternatively, if you want to mutate the value in place, make it something mutable, like a list, and make the before functions mutate the value instead of just rebinding the global to a different value.

But what you're asking for is a decorator that can reach up to the calling frame, figure out what expressions were evaluated to get the arguments, and force them to be re-evaluated. That's just silly.


If you really, really wanted to do that, the only way to do it would be to capture and interpret the bytecode in sys._getframe(1).f_code.

At least in CPython 2.7, you will get some sequence of codes that pushes your decorated function onto the stack (a simple LOAD_NAME or LOAD_NAME in the typical case, but not necessarily), then a sequence of codes to evaluate the expressions, then a CALL_FUNCTION/CALL_FUNCTION_VAR/etc. So, you can walk backward, simulating the operations, until you've found the one that pushed your function onto the stack. (I'm not sure how to do this in a foolproof way, but it ought to be doable. Then, build a new code object that just pushes your function with a LOAD_CONST and repeats all the operations after it (and then returns the value). Then wrap that code in a function with the exact same environment as the caller, then call that new function and return its value, instead of calling the wrapped function directly.

Here's an example:

def call_do_thing(b):
    global a
    b += a
    return do_thing(a * b)

The psuedo-bytecode is:

LOAD_FAST b
LOAD_GLOBAL a
INPLACE_ADD
STORE_FAST b
LOAD_GLOBAL do_thing
LOAD_GLOBAL a
LOAD_FAST b
BINARY_MULTIPLY
CALL_FUNCTION 1
RETURN_VALUE

In this case, finding the function call is easy, because it used a LOAD_GLOBAL. So, we just need to take all the ops from there to the RETURN_VALUE and wrap them up in a new function to call instead of the one we've been given, and a will be re-evaluated in the new globals.

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