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.

Hyperboreus
  • 31,997
  • 9
  • 47
  • 87

3 Answers3

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.

ruakh
  • 175,680
  • 26
  • 273
  • 307
  • 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 Feb 03 '13 at 08:49
  • @Hyperboreus: Are Python interpreters not allowed to optimize that? – ruakh Feb 03 '13 at 19:22
  • 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. – Hyperboreus Feb 03 '13 at 20:41
  • 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 Feb 03 '13 at 20:43
  • @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.) – ruakh Feb 03 '13 at 21:09
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.

Hyperboreus
  • 31,997
  • 9
  • 47
  • 87
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
davidchambers
  • 23,918
  • 16
  • 76
  • 105