2

Is there a way in python to intercept (and change) the return value of an already compiled function?

The basic idea is: I have a function

def a (n, acc):
 if n == 0: return acc
 return a (n - 1, acc + n)

and I want to patch it, so that it behaves like this:

def a (n, acc):
 if n == 0: return acc
 return lambda: a (n - 1, acc + n)

Is it possible to write a function f, such as f (a) yields a function as in the second code snippet?

I can patch the function via inspect if python can locate its source and then return the newly compiled patched function, but this won't help much.

asked Feb 3, 2013 at 6:46

3 Answers 3

2

If I'm understanding correctly what you want, it is theoretically impossible; the transformation that you seem to be describing is one that would have different effects on equivalent functions, depending on superficial details of their source-code that likely won't be preserved in the compiled form. For example, consider the following two versions of a given function:

def a (n, acc):
 print('called a(%d,%d)' % (n, acc))
 if n == 0: return acc
 return a (n - 1, acc + n)
def a (n, acc):
 print('called a(%d,%d)' % (n, acc))
 if n == 0: return acc
 ret = a (n - 1, acc + n)
 return ret

Clearly they are functionally identical. In the source code, the only difference is that the former uses return directly on a certain expression, whereas the latter saves the result of that expression into a local variable and then uses return on that variable. In the compiled form, there need be no difference at all.

Now consider the "patched" versions:

def a (n, acc):
 print('called a(%d,%d)' % (n, acc))
 if n == 0: return acc
 return lambda: a (n - 1, acc + n)
def a (n, acc):
 print('called a(%d,%d)' % (n, acc))
 if n == 0: return acc
 ret = a (n - 1, acc + n)
 return lambda: ret

Clearly these are very different: for example, if n is 3 and acc is 0, then the former prints called a(3,0) and returns a function that prints called a(2,3) and returns a function that prints called a(1,5) and returns a function that prints called a(0,6) and returns 6, whereas the latter prints called a(3,0) and called a(2,3) and called a(1,5) and called a(0,6) and returns a function that returns a function that returns a function that returns 6.

The broader difference is that the first "patched" function performs one step of the computation each time the new return-value is called, whereas the second "patched" version performs all steps of the computation during the initial call, and simply arranges a series of subsequent calls for the sake of entertainment. This difference will matter whenever there's a side-effect (such as printing a message, or such as recursing so deeply that you overflow the stack). It can also matter if the caller introduces a side-effect: note that these functions will only be recursive until some other bit of code redefines a, at which point there is a difference between the version that plans to continue re-calling a and the version that has already completed all of its calls.

Since you can't distinguish the two "unpatched" versions, you obviously can't generate the distinct "patched" versions that your transformation implies.

answered Feb 3, 2013 at 7:35
Sign up to request clarification or add additional context in comments.

5 Comments

You can well distinguish the two unpatched versions. The first one ends with CALL_FUNCTION RETURN_VALUE and the latter one ends with CALL_FUNCTION STORE_FAST LOAD_FAST RETURN_VALUE.
@Hyperboreus: Are Python interpreters not allowed to optimize that?
I am not sure if creating a local variable can be optimized. I don't now if they are allowed to do it. Mine doesn't. My last comment is from the disassembly.
The important difference here in context is that one of your examples is tail recursive while the other isn't as after the recursive call there's still an assignment.
@Hyperboreus: That's true of the source text, but is it necessarily true of the compiled form? (By the way, tail recursion is not special in Python, in that implementations are not permitted to perform tail-call optimization. So "is tail recursive" vs. "is not tail recursive" is not a meaningful distinction when two Python functions are otherwise equivalent.)
1

Thank for your input. I wasn't seeing the obvious:

def inject (f):
 def result (*args, **kwargs):
 return lambda: f (*args, **kwargs)
 return result

I accepted davidchambers's answer as he pushed me into the right direction.

answered Feb 3, 2013 at 7:18

Comments

0

Luke's answer is very detailed, but this may still be helpful:

>>> def f(*args, **kwargs):
... return lambda: a(*args, **kwargs)
...
>>> f(10, 0)()
55
answered Feb 3, 2013 at 7:08

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.