6

For a decorator I am writing I would like to manipulate a specific named parameter of a function. Consider the following decorator:

def square_param(param):
    def func_decorator(func):
        def func_caller(*args,**kwargs):
            kwargs[param] = kwargs[param] * kwargs[param]
            return func(*args,**kwargs)
    return func_caller
return func_decorator

Applied on the next function:

@square_param('dividend')
def quotient(divisor=1,dividend=0):
    return dividend/divisor

This will work if dividend is called as a keyword argument e.g.:

>>> quotient(dividend=2)
4

However, when given as a positional argument this will fail.

>>> quotient(3,4)
TypeError: quotient() got multiple values for keyword argument 'dividend'

With Python 3 I could solve this by forcing the parameter to be always given as a keyword:

@square_param('dividend')
def quotient(divisor=1,*,dividend=0):
    return dividend/divisor

but I would like to support Python 2 and also I would like to put as little restrictions on the function.

Is there a way that I can fix this behaviour in my decorator?

Peter Smit
  • 27,696
  • 33
  • 111
  • 170
  • 4
    Inspect the inspect module. It might have what you need for this. Have a look at getargspec. – Dirk Jul 05 '11 at 07:40

3 Answers3

4

Firstly, your square_param decorator doesn't work because it doesn't return the functions. It needs to be:

def square_param(param):
    def func_decorator(func):
        def func_caller(*args,**kwargs):
            kwargs[param] = kwargs[param] * kwargs[param]
            return func(*args,**kwargs)
        return func_caller
    return func_decorator

Now I took @Dirk's advice and looked into the inspect module. You can do it by checking first if the parameter is one of the function's positional arguments, and second if that positional argument has been specified, and then modifying that argument position. You also need to make sure you only modify kwargs if the parameter was supplied as a keyword argument.

import inspect

def square_param(param):
    def func_decorator(func):
        def func_caller(*args,**kwargs):
            funparams = inspect.getargspec(func).args
            if param in funparams:
                i = funparams.index(param)
                if len(args) > i:
                    args = list(args)   # Make mutable
                    args[i] = args[i] * args[i]
            if param in kwargs:
                kwargs[param] = kwargs[param] * kwargs[param]
            return func(*args,**kwargs)
        return func_caller
    return func_decorator
Peter Smit
  • 27,696
  • 33
  • 111
  • 170
mgiuca
  • 20,958
  • 7
  • 54
  • 70
  • Thanks, this is exactly what I was looking for! About the returning of the functions, that was indeed a small stupid error made in this example. – Peter Smit Jul 05 '11 at 08:01
2

even without using Inspect we can get function params

>>> func = lambda x, y, args: (x, y, {})
>>> func.func_code.co_argcount
3
>>> func.func_code.co_varnames
('x', 'y', 'args')
Yajushi
  • 1,175
  • 2
  • 9
  • 24
  • Thanks! Could you maybe add a link where the func_code members are documented? I can't find that. – Peter Smit Jul 05 '11 at 09:10
  • You're welcome! "func_code is the code object representing the compiled function body." You can refer the "Callable types" section on [link](http://docs.python.org/reference/datamodel.html) for more details. – Yajushi Jul 05 '11 at 09:24
0

This may only be tangentially related, but I found it useful to solve a similar problem. I wanted to meld *args and **kwargs into a single dictionary so that my following code could process without regard to how the args came in, and I didn't want to mutate the existing kwargs variable, otherwise I just would have use kwargs.update().

all_args = {**kwargs, **{k: v for k, v in zip(list(inspect.signature(func).parameters), args)}}

# optionally delete `self`
del (all_args['self'])

Update: While this works, this answer has a better technique. In part:

bound_args = inspect.signature(f).bind(*args, **kwargs)
bound_args.apply_defaults()
cjbarth
  • 4,189
  • 6
  • 43
  • 62