7

I run a decorator demo below.

def logger(func):
    def inner(*args, **kwargs):
        print(args)
        print(kwargs)
        return func(*args, **kwargs)
    return inner

@logger
def foo1(a, b, c, x=2, y=1):
    print(x * y)

foo1(6,7,8)

output is:

(6, 7, 8)
{}
2

Why is the dict empty? I think it should be {'x':2, 'y':1}

glibdud
  • 7,550
  • 4
  • 27
  • 37
yao Ge
  • 73
  • 1
  • 3
  • This question has answers here: https://stackoverflow.com/questions/31728346/passing-default-arguments-to-a-decorator-in-python but I can't flag it because of the bounty. – Boris Verkhovskiy Jan 28 '21 at 16:49
  • 1
    @Boris I answered to what yao Ge wanted 2 years ago. Now cosmic_inquiry added bounty to add more details to that answer, not the solution of that behavior ;) – wowkin2 Jan 28 '21 at 17:11

3 Answers3

5

That's because of no kwargs provided in a function call. And decorator logger know nothing about that and what function will use. It is kind a "proxy" between kwargs provided there and real call.

See examples below:

# kwargs are not provided (not redefined), function `foo1` will use default.
>>> foo1(6, 7, 8)
(6, 7, 8)
{}
2

# new kwargs are provided and passed to decorator too
>>> foo1(6, 7, 8, x=9, y=10)
(6, 7, 8)
{'x': 9, 'y': 10}
90

This is something similar to:

def foo1(a, b, c, x=2, y=1):
    print(x * y)


def logger(func):
    def inner(*args, **kwargs):
        print(args)
        print(kwargs)
        return func(*args, **kwargs)
    return inner


wrapped_foo1 = logger(foo1)
wrapped_foo1(6,7,8)

Or even simplified to the following, when you can clearly see the problem:

def foo1_decorated(*args, **kwargs):
    print(args)  # <-- here it has no chance to know that `x=2, y=1`
    print(kwargs)
    return foo1(*args, **kwargs)

foo1_decorated(6, 7, 8)
wowkin2
  • 5,895
  • 5
  • 23
  • 66
  • @cosmic-inquiry, added more examples. The last one should show that they are just different kwargs. And decorator (logger) works like proxy. – wowkin2 Jan 28 '21 at 16:28
  • You explained the problem, but it would also be great if you explained how to achieve what yao Ge wanted, you can just copy/paste from the question I linked. – Boris Verkhovskiy Jan 28 '21 at 17:03
  • @Boris yao Ge wanted just explanation "why?" not workaroung :) – wowkin2 Jan 28 '21 at 17:12
2

The problem is that the default values for arguments are filled in by the wrapped function object when you call it, because only the wrapped function knows them (they are stored in __defaults__ and __kwdefaults__). If you want your decorator to know about them too, you have to mimic what the wrapped function object would do. For this task you can use the inspect module:

from inspect import signature

def logger(func):
    sig = signature(func)
    def inner(*args, **kwargs):
        arguments = sig.bind(*args, **kwargs)    # these 2 steps are normally handled by func
        arguments.apply_defaults()
        print(func, "was called with", arguments)
        return func(*args, **kwargs)
    return inner

@logger
def foo1(a, b, c, x=2, y=1):
    print(x * y)

foo1(6,7,8)

Output:

<function foo1 at 0x7f5811a18048> was called with <BoundArguments (a=6, b=7, c=8, x=2, y=1)>
2

If you want to access the arguments, read more about it in the docs.

Deric
  • 180
  • 7
0

That dictionary is empty because you have not passed any kwargs in foo1.

To get x and y instead of empty dictionary you can use

foo1(6,7,8, x=2, y=3) # x and y are printed while printing kwargs

instead of

foo1(6,7,8)  # no x and y values are passed so empty dict is print while printing kwargs

Note that you should only use variable x and y. Any other variables will cause error.

The process that is exactly happening is this:

1. foo1 function is tried to called
2. Due to presence of @logger, logger function is called first
3. foo1 function is passed to logger function.
4. inner function takes both type of arguments of foo1 function.
4. *args accepts arguments that are comma separated and should not
   contain key = value type of argument
5. **kwargs accepts arguments that are only key = value type
6. Since you have passed 6,7,8, they are all treated as *args 
7. To pass as **kwargs, you have to pass key = value in foo1 parameters.
8. *args and ** kwargs values are printed
9. foo1 function is called 
10. It executes code inside foo1
Prakash Dahal
  • 4,388
  • 2
  • 11
  • 25