1

I can't figure out how to do this, and frankly, I don't know if it's possible.

I want to write a decorator that changes the way a function is called. It's easiest to see with example code:

def my_print(*args, **kwargs):
    print(args[0].upper())

@reroute_decorator('print', my_print)
def my_func():
    print('normally this print function is just a print function...')
    print('but since my_func is decorated with a special reroute_decorator...')
    print('it is replaced with a different function, and its args sent there.')

my_func()
# NORMALLY THIS PRINT FUNCTION IS JUST A PRINT FUNCTION...
# BUT SINCE MY_FUNC IS DECORATED WITH A SPECIAL REROUTE_DECORATOR...
# IT IS REPLACED WITH A DIFFERENT FUNCTION, AND ITS ARGS SENT THERE.

Is a decorator with this kind of functionality even possible in python?

Now, I don't really need this if it's too complex, I just can't figure out how to do it in a simple way.

Is this kind of a problem trivial? Or is it really complex?

MetaStack
  • 3,266
  • 4
  • 30
  • 67
  • It looks like you want to use [`unittest.mock.patch`](https://docs.python.org/3/library/unittest.mock.html). – Klaus D. Oct 28 '19 at 20:11
  • Should it consider only `print` calls? Do you expect another use cases? – RomanPerekhrest Oct 28 '19 at 20:13
  • @RomanPerekhrest I think it should be able to 'reroute' any function to any other function. – MetaStack Oct 28 '19 at 20:24
  • If you want it to work only with global functions, it's not that complex. If you want it to work in general, it's very complex. – JBGreen Oct 28 '19 at 20:24
  • Actually scratch that, even for global functions it is fairly complex, if you want it to work across modules. – JBGreen Oct 28 '19 at 20:31
  • 1
    While this is related to the question, it might not be the answer you are looking for: a good series (3 videos) that will clarify a lot of what decorators can do in Python can be found [here](https://www.youtube.com/watch?v=PJQ5XopgNog&list=PLR-r0edywujd8D-R2Kue1C_wYEK_4Ii71&index=11). Around ~20 minutes in total, and worth watching to gain the **intuition** behind decorators. – felipe Oct 28 '19 at 21:15
  • @JBChouinard. Sort of. You can kind-of spoof anything as global without actually contaminating any globals. But of course there are corner cases and unexpected side effects with that too. – Mad Physicist Oct 28 '19 at 21:17

2 Answers2

2

To elaborate on @Dan D.'s answer, you would create a new function object to replace the original, something like this:

from types import FunctionType

def reroute_decorator(**kwargs):
    def actual_decorator(func):
        globals = func.__globals__.copy()
        globals.update(kwargs)
        new_func = FunctionType(
            func.__code__, globals, name=func.__name__,
            argdefs=func.__defaults__, closure=func.__closure__)
        new_func.__dict__.update(func.__dict__)
        return new_func
    return actual_decorator

The only catch here is that the updated function object is the only one that will see whatever kwargs you passed in, since they will be spoofed into globals. Additionally, any modifications you make to the module after calling the decorator function will not be visible to the decorated function, but that should not be an issue. You can go a layer deeper and create a proxy dictionary that would allow you to interact normally with the original, except for keys you explicitly defined, like print, but that's a bit out of scope here.

I've updated your print implementation to be a bit more general, and made the input to the decorator function more pythonic (less MATLABy):

def my_print(*args, **kwargs):
    print(*(str(x).upper() for x in args), **kwargs)

@reroute_decorator(print=my_print)
def my_func():
    print('normally this print function is just a print function...')
    print('but since my_func is decorated with a special reroute_decorator...')
    print('it is replaced with a different function, and its args sent there.')

Which results in:

>>> my_func()
NORMALLY THIS PRINT FUNCTION IS JUST A PRINT FUNCTION...
BUT SINCE MY_FUNC IS DECORATED WITH A SPECIAL REROUTE_DECORATOR...
IT IS REPLACED WITH A DIFFERENT FUNCTION, AND ITS ARGS SENT THERE.
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
2

You can create a new function with an updated globals dictionary so that to that function it appears that the global was bound to the desired value.

Note that this is weaker than actual dynamic scope as any functions called by the function will see the original bindings and not the modified one.

See namespaced_function referenced in How does Python's types.FunctionType create dynamic Functions?

Dan D.
  • 73,243
  • 15
  • 104
  • 123
  • I've taken the liberty of expanding on your proposed solution. +1. Couldn't figure out a way to use the E in LEGB, so gotta settle for G. – Mad Physicist Oct 28 '19 at 21:15