Could anybody explain me the workflow of the decorator and how this example works?

Question:

def log(func):
    def wrapper(*args, **kwargs):
        print(args)
        val = func(*args, **kwargs)
    return wrapper



@log
def run(a, b, c):
    print(a+b+c)
    
run(1,5,c=9)

The function ‘run’ has three arguments and we run the function at the bottom. The function has decorator and I am trying to understand how the decorator works in this case.

what does it contain the argument ‘func’ in the log.
How does it work the ‘wrapper’ and where does it know the arguments.
How does it work
val = func(*args, **kwargs)

I understand the process about 70% but I still don’t feel sure enough so i hope someone can take some time to explain the process.

Asked By: Dobri

||

Answers:

Decorators are used using @ syntax. For example,

@log
def a_fun(a, b, c):
   ...

This calls, same as (that is, equivalent to but more handier than):

a_fun = log(a_fun)

From below:

def log(func):
    def wrapper(*args, **kwargs):
        print(args)
        val = func(*args, **kwargs)
    return wrapper



@log
def run(a, b, c):
    print(a+b+c)
    
run(1,5,c=9)

To start with :

def log(func):

This is defining a function, log. It take a single argument, func. Here it is equal to a callable (a function).

Note from above, we had:

... = log(a_fun)

Hence, it can be clearly understood that the value of func, in this specific example, is equal to a_fun exactly (passed in value).

Next, we go to :

def wrapper(*args, **kwargs):

This is declaring a function, of name wrapper. It takes generic parameters, that is *args, **kwargs which is familiar ppython syntax, when you don’t necessarily know which arguments or keywords that a function is going to accept. Thus, it is defined as such in order to be as generic and inclusive as possible, so as to apply to any kind of function (taking zero to hundred+ arguments, and same with keyword arguments for example).

Note that type of *args is a tuple[Any...] meaning it is a tuple of one of more elements (of any type). Type of **kwargs is a dict[str, Any], meaning it is a dict containing parameter name as key (str obviously, as we cannot define a func like def my_fun(123: int) for instance) and value as any type (since it is not known here).

This can be confirm with:

assert type(args) is tuple
assert type(kwargs) is dict

Finally:

val = func(*args, **kwargs)

This is saying, pass or forward the captured input *args and **kwargs, and unpack and pass them as-is to the callable or function func.

Then val here is the return value of the function func – if it does not return anything, this will be None, else it will be the returned value of the function itself.

As mentioned from above, the value of func in this context is exactly equal to run. Thusly, the value of *args is equal to the evaluated value of (a, b, c) and the value of **kwargs depends on how the function is invoked – for example, run(..., 0, c=2) would result in the kwargs object being populated in this specific use case as dict(c=2) as that is how the function or callable is being invoked. Conversely, the args object in this case would then be a tuple (..., 0) as those are the specific positional arguments (separate from named or keyword arguments, which are captured into kwargs as mentioned above).

Note that in this example, run does not return anything:

def run(a, b, c):
    print(a+b+c)

More specifically, it returns None, as print(...) always returns None.

Hence,

val = ...

from the above, the value of val will thus be captured as None.

assert val is None

This can be used to confirm (or disprove) the above point.

Lastly but not leastly, we cannot forget the return statement that also comes into play here:

return wrapper

This is saying, from the decorator function log, I want to return a new function wrapper. So that, the meaning is, the original (or decorated) function will have its value overwritten.

So to clarify, remember from above the following syntax:

@log
def a_fun(a, b, c):
   ...

That as mentioned, this is then equivalent to:

a_fun = log(a_fun)

So the value log(a_fun), is whatever the return value of the decorator function log is.

In this specific case, as mentioned the return value of log is wrapper, as the return statement does indicate that as well.

Thus, the evaluated value of the original (decorated) function a_fun would then be:

a_fun = wrapper

Hence, we have prove that the original or un-decorated function a_fun (the underlying callable that is) now have its value overwritten by the decorator, and that its new assigned value is thus wrapper. So we have an equivalency here of a_fun == wrapper after the decorator @log is applied.

Hope that is helpful, or at least clarifies it rather than obfuscates the problem statement and what is happening exactly in the code. At least, the goal here is to not muddy the waters, but rather to provide clarity through openness and the sharing of information such not necessarily that understanding is achieved, but that at the least we thus end up at a better place and situation than where we initially started or originated from.

Answered By: rv.kvetch
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.