Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I pass arguments to decorator, process there, and forward to decorated function?

I recently decided to learn Python decorators, and followed this howto. Especially the exemple under the section "passing arguments to decorators". What I want to do is (1) the decorator function should accept some arguments (decarg), (2) process them, and (3) call the decorated function with arguments (funarg). I see how to give arguments to the decorator, but I haven't seen any example, where the arguments of the decorated function become available only in the decorator. When I call the decorated function, I completely not sure whit what arguments I should do it, as its arguments are to be calculated in the decorator function. Here is an example based on the mentioned howto:

def dec(decarg):
    def _dec(fun):
        funarg = decarg + 7
        def _fun(funarg):
            return fun(funarg)
        return _fun
    return _dec

def main(mainarg):
    decarg = mainarg + 2
    @dec(decarg)
    def fun1(funarg):
        return funarg + 3
    return fun1(decarg)

main(1)

This returns 6, when I expect 13. I want the argument incremented by 2 in the main(), and by 7 in _dec(), which should pass this variable to the decorated function, which adds 3 to it.

After, I read this and this howtos, and using their approach I created an example which works as I imagined:

class dec(object):

    def __init__(self, fun):
        self.fun = fun

    def __call__(self, decarg):
        funarg = decarg + 7
        return self.fun(funarg)

def main(mainarg):
    decarg = mainarg + 2
    @dec
    def fun1(funarg):
        return funarg + 3
    return fun1(decarg)

main(1)

So now my question is: how to do the same with the first type of notation, where the decorator is not a class, but a function? Could you please clarify, how it works, which arguments and when are to be passed to the __init__() and what to the __call__() method of the decorator?

like image 769
deeenes Avatar asked Oct 17 '15 19:10

deeenes


People also ask

Can you pass in arguments to decorators?

To make matters more confusing, you can also see that the wrapped() inner function has "catch-all" arguments which are passed directly into the decorated function and should never be mixed with arguments meant for the decorator. So there is really no place where decorator arguments can be added.

How do you pass an argument to a function?

You can pass data to functions so they can work on that data. For example, you can create a function named adder() that you want to add two integers and display the results. Now in the body of the function, you can refer to the first argument as x and the second argument as y.

How do you pass a value dynamically in Python?

Dynamic Function Arguments We simply can make solve_for() accept *args and **kwargs then pass that to func() . Of course, you will need to handle the arguments in the function that will be called.


2 Answers

The reason is immediate after considering how the decorator transforms the function and that functions are objects themselves in Python.

Let's start from the latter.

Functions are objects:

This is immediate when we consider the meaning of two pairs of parenthesis after a function name. Consider this simple example (Python 3):

def func(x):
    def func2(y):
        return x + y + 1
    return func2

result = func(5)(10)
print(result)  # 15

Here "func" returns a function object "func2" and therefore you can use:

func(5)(10)

You can view this as calling first

func(5)

and applying "(10)" to the resulting object that is a function! So you have:

func2(10)

Now since both "x" and "y" are defined, "func2" can return the final value to "result".

Remember, this is all possible because functions are object themselves and because "func" returns a function object

func2

and not its result (it is not invoking the function on its own)

func2()

In short, that means that with wrapped functions the second set of arguments is for the inner function (if the wrapper returns the inner function object).

Decorators:

In your example, "main" calls "fun1" in the last line with

return fun1(decarg)

Due to the decorator

@dec(decarg)

In reality you can think of "fun1" as:

fun1 = dec(decarg)(fun1)

Therefore, the last line in "main" is equivalent to:

return dec(decarg)(fun1)(decarg)

With the previous explanation it should be trivial to find the problem!

  • dec(decarg) gets executed and returns a "_dec" function object; note that this "decarg" is the one passed in the first parenthesis and thus in the decorator.
  • _dec(fun1) gets executed and returns a "_fun" function object.
  • _fun(decarg) gets executed and invokes fun1(decargs) with the return statement and this will correctly translate in fun1(3) that is the result you get; note that this "decarg" is the one passed in the third parenthesis and thus when you invoke "fun1" in main.

You don't get 13 as a result because you don't invoke "fun1" with the result from

funarg = decarg + 7

as argument, but rather you invoke it with "decarg" that is passed to "_fun" as positional argument (funarg=decarg) from main.

Anyway, I have to thank you for this question, because I was looking for a neat way to pass an argument to a decorator only when invoking a function, and this works very nicely.

Here is another example that might help:

from functools import wraps

def add(addend):
    def decorator(func):
        @wraps(func)
        def wrapper(p1, p2=101):
            for v in func(p1, p2):
                yield v + addend
        return wrapper
    return decorator


def mul(multiplier):
    def decorator(func):
        @wraps(func)
        def wrapper(p1, p2=101):
            for v in func(p1, p2):
                yield v * multiplier
        return wrapper
    return decorator


def super_gen(p1, p2=101, a=0, m=1):
    @add(a)
    @mul(m)
    def gen(p1, p2=101):
        for x in range(p1, p2):
            yield x
    return gen(p1, p2)
like image 60
Bertone Avatar answered Oct 16 '22 12:10

Bertone


Ok here is a simple explaination:

A simple decorator (no parameters) is a function that takes a function as argument and returns another function that will be called in place of the original one

A decorator that accepts a parameter is a function that takes that parameter and returns a simple decorator

Here you can build a simple decorator that adds 7 to the parameter of the decorated function:

def add7(func):
    '''takes a func and returns a decorated func adding 7 to its parameter'''
    def resul(x):
        return func(x + 7)
    return resul

You can use it that way:

def main(mainarg):
    decarg = mainarg + 2
    @add7
    def fun1(funarg):
        return funarg+3
    return fun1(decarg)

main(1)

it returns as expected 13.

But you can easily build a parameterized decorator that will add an arbitrary value, but adding a level for processing the parameter:

def adder(incr):
    '''Process the parameter (the increment) and returns a simple decorator'''
    def innerdec(func):
        '''Decorator that takes a function and returns a decorated one
that will add the passed increment to its parameter'''
        def resul(val):
            return func(val + incr)
        return resul
    return innerdec

You will then use it that way

 def main(mainarg):
    decarg = mainarg + 2
    @adder(7)
    def fun1(funarg):
        return funarg + 3
    return fun1(decarg)

main(1)

still returns 13

like image 41
Serge Ballesta Avatar answered Oct 16 '22 11:10

Serge Ballesta