yao Ge
yao Ge

Reputation: 73

‘kwargs’ is empty in python decorator

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}

Upvotes: 7

Views: 6753

Answers (3)

Deric
Deric

Reputation: 180

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.

Upvotes: 2

wowkin2
wowkin2

Reputation: 6355

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)

Upvotes: 5

Prakash Dahal
Prakash Dahal

Reputation: 4875

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

Upvotes: 0

Related Questions