How can a decorator pass variables to a function without changing its signature?

Let me first admit that what I want to do can be considered something from stupid to evil, but I want to find out if I can do this in Python anyway.

Say I have a function decorator that accepts keyword arguments that define variables, and I want to access these variables in a wrapped function. I could do something like this:

def more_vars(**extras): def wrapper(f): @wraps(f) def wrapped(*args, **kwargs): return f(extras, *args, **kwargs) return wrapped return wrapper 

Now I can do something like:

 @more_vars(a='hello', b='world') def test(deco_vars, x, y): print(deco_vars['a'], deco_vars['b']) print(x, y) test(1, 2) # Output: # hello world # 1 2 

What I don't like about this is that when you use this decorator, you need to change the signature of the function call by adding an additional variable in addition to the slap on the decorator. In addition, if you look at help for this function, you will see an additional variable that you do not expect to use when calling the function:

 help(test) # Output: # Help on function test in module __main__: # # test(deco_vars, x, y) 

This means that the user must call a function with 3 parameters, but obviously this will not work. Thus, you will also need to add a message to the docstring indicating that the first parameter is not part of the interface, it is just an implementation detail and should be ignored. However, this crappy one. Is there a way to do this without hanging these variables on something in a global scope? Ideally, I would like it to look like this:

 @more_vars(a='hello', b='world') def test(x, y): print(a, b) print(x, y) test(1, 2) # Output: # hello world # 1 2 help(test) # Output: # Help on function test in module __main__: # # test(x, y) 

I am only happy with the Python 3 solution if it exists.

+7
python
source share
3 answers

You can do this with some trickery that inserts the variables passed to the decorator into local function variables:

 import sys from functools import wraps from types import FunctionType def is_python3(): return sys.version_info >= (3, 0) def more_vars(**extras): def wrapper(f): @wraps(f) def wrapped(*args, **kwargs): fn_globals = {} fn_globals.update(globals()) fn_globals.update(extras) if is_python3(): func_code = '__code__' else: func_code = 'func_code' call_fn = FunctionType(getattr(f, func_code), fn_globals) return call_fn(*args, **kwargs) return wrapped return wrapper @more_vars(a="hello", b="world") def test(x, y): print("locals: {}".format(locals())) print("x: {}".format(x)) print("y: {}".format(y)) print("a: {}".format(a)) print("b: {}".format(b)) if __name__ == "__main__": test(1, 2) 

Can you do this? Of course! Should you do this? Probably not!

(Available code here .)

+2
source share

I don't like the idea of ​​injecting locals into a namespace. While this is possible, it might be unpleasant to handle collisions between new localized locales and names that are already present in this function.

Is it permissible to store them as attrs on a function object? So they are called.

 from functools import wraps def more_vars(**extras): def wrapper(f): @wraps(f) def wrapped(*args, **kwargs): return f(*args, **kwargs) for k,v in extras.items(): setattr(wrapped, k, v) return wrapped return wrapper @more_vars(a='hello', b='world') def test(x, y): print(test.a, test.b) print(x, y) 
0
source share

It seems that the only problem is that help shows the signature of raw test as the signature of the wrapped function, and you don't want it.

The only reason wraps happen (or rather update_wrapper , calling wraps ) explicitly copies this from wrapper to wrapper.

You can decide exactly what you are doing and do not want to copy. If what you want to do differently is simple enough, it's just a matter of filtering the material from the standard WRAPPER_ASSIGNMENTS and WRAPPER_UPDATES . If you want to change other things, you may need to fork update_wrapper and use your own version, but functools is one of those modules that has a link to the source at the top of the document, because it is designed to be used as a readable sample code.

In your case, it may just be a wraps(f, updated=[]) question, or you might want to do something fantastic, for example, use inspect.signature to get the signature of f and change it to remove the first parameter, and build a shell clearly around this to fool even the inspect module.

0
source share

All Articles