GajaLulu
GajaLulu

Reputation: 37

Why is **kwargs value in a function decorator different from the value in a function?

I'm trying to write a function decorator, that prints arguments, with which a function was called and I noticed one thing. If we create decorator, that simply prints kwargs, it will work. So

def counted(fn):
    def wrapper(*args, **kwargs):
        print kwargs
        return fn(*args, **kwargs)
    return wrapper
@counted
def foo(a, b = 4, c = 'blah-blah', *args, **kwargs):
    return

foo(a=1, b=2, c=3, args=[4, 5], kwargs={'d': 6, 'g': 12.9})

outputs

{'a': 1, 'c': 3, 'b': 2, 'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}

However, if the function is called without decorator, but the arguments are printed inside the function, output differs:

#@counted
def foo(a, b = 4, c = 'blah-blah', *args, **kwargs):
print kwargs    #added this line
return

Output:

{'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}

Why is that? Why does print kwargs in a decorator prints first three arguments, but don't in a function?

Upvotes: 1

Views: 275

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121972

kwargs captures any key=value parameter that wasn't specifically named in your function signature. foo names a, b and c specifically, so those are not captured, but your decorator wrapper names none, so everything is captured in kwargs. Note that this includes the args=... and kwargs=... parameters you passed into your call.

You need to add print to both places and you'll see it is not the decorator that is doing anything special here:

>>> @counted
... def foo(a, b = 4, c = 'blah-blah', *args, **kwargs):
...     print kwargs
...
>>> foo(a=1, b=2, c=3, args=[4, 5], kwargs={'d': 6, 'g': 12.9})
{'a': 1, 'c': 3, 'b': 2, 'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}
{'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}

You also don't print args, so you don't see what positional arguments are passed along. Let's add some more print statements to show what is really going on:

def counted(fn):
    def wrapper(*args, **kwargs):
        print 'counted args: {!r}'.format(args)
        print 'counted kwargs: {!r}'.format(kwargs)
        return fn(*args, **kwargs)
    return wrapper

@counted
def foo(a, b = 4, c = 'blah-blah', *args, **kwargs):
    print 'foo a, b, c: {!r}, {!r}, {!r}'.format(a, b, c)
    print 'foo args: {!r}'.format(args)
    print 'foo kwargs: {!r}'.format(kwargs)

Running this you'll get a much better picture:

>>> foo(a=1, b=2, c=3, args=[4, 5], kwargs={'d': 6, 'g': 12.9})
counted args: ()
counted kwargs: {'a': 1, 'c': 3, 'b': 2, 'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}
foo a, b, c: 1, 2, 3
foo args: ()
foo kwargs: {'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}

Again, note that both 'args' and 'kwargs' are themselves keyword arguments; they both end up as keys in the kwargs dictionary. You could name them anything else and they'll still end up with the new names:

>>> foo(a=1, b=2, c=3, spam=[4, 5], eggs={'d': 6, 'g': 12.9})
counted args: ()
counted kwargs: {'a': 1, 'c': 3, 'b': 2, 'eggs': {'d': 6, 'g': 12.9}, 'spam': [4, 5]}
foo a, b, c: 1, 2, 3
foo args: ()
foo kwargs: {'eggs': {'d': 6, 'g': 12.9}, 'spam': [4, 5]}

You didn't specify values for the *args and **kwargs catch-all variables there, you specified more arbitrary keyword arguments to be captured in kwargs.

The remaining arguments, a, b and c are 'captured' by the named parameters of the foo function. You defined those separately in foo so they are assigned to specifically. They are not part of the wrapper() definition, so they couldn't be captured there.

Note that you didn't pass any positional parameters to the call, so the *args variable is empty. Pass in some positional arguments instead:

>>> foo(1, 2, 3, 42, args=[4, 5], kwargs={'d': 6, 'g': 12.9})
counted args: (1, 2, 3, 42)
counted kwargs: {'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}
foo a, b, c: 1, 2, 3
foo args: (42,)
foo kwargs: {'args': [4, 5], 'kwargs': {'d': 6, 'g': 12.9}}

Now the first 3 values end up in a, b and c still, but the extra positional value 42 is still available in args in foo. In the wrapper, where there were no explicitly named arguments, they all ended up in *args.

If you expected your args=[..] and kwargs={...} values to be applied as separate arguments, you need to use the same * and ** prefixes when calling without using the capturing names:

>>> foo(1, 2, 3, 42, *[4, 5], **{'d': 6, 'g': 12.9})
counted args: (1, 2, 3, 42, 4, 5)
counted kwargs: {'d': 6, 'g': 12.9}
foo a, b, c: 1, 2, 3
foo args: (42, 4, 5)
foo kwargs: {'d': 6, 'g': 12.9}

Now 4 and 5 also appear in args, and the 'd' and 'g' keys appear in the kwargs dictionaries directly.

Upvotes: 1

Related Questions