Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

‘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}

like image 254
yao Ge Avatar asked Aug 31 '18 14:08

yao Ge


2 Answers

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)
like image 187
wowkin2 Avatar answered Sep 27 '22 20:09

wowkin2


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.

like image 36
Deric Avatar answered Sep 27 '22 20:09

Deric