Passing default arguments to a decorator in python
Asked Answered
I

3

15

I am trying to find a way to pass my function's default arguments to the decorator. I have to say I am fairly new to the decorator business, so maybe I just don't understand it properly, but I have not found any answers yet.

So here's my modified example from the Python functools.wraps manual page.

from functools import wraps
def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
            print('Calling decorated function')
            print('args:', args)
            print('kwargs:', kwds)
            return f(*args, **kwds)
    return wrapper

@my_decorator
def example(i, j=0):
    """Docstring"""
    print('Called example function')

example(i=1)

I want the j=0 to be passed, too. So the output should be:

Calling decorated function
args: ()
kwargs: {'i': 1, 'j': 0}
Called example function

But instead I get

Calling decorated function
args: ()
kwargs: {'i': 1}
Called example function
Isador answered 30/7, 2015 at 15:44 Comment(2)
j=0 is passed, but not inside wrapper. If you print i, j inside example, you'll see that it is there. You could use e.g. inspect.getargspec(f) to see what defaults are set on the function being decorated, but why do you need to access the default in wrapper?Baird
I know its passed into example, but I need j to be passed to the wrapper because it is needed for calculations i am doing with with many several functions. But generally using inspect.getargspec(f) would work, thank you.Isador
I
11

Default arguments are part of the function's signature. They do not exist in the decorator call.

To access them in the wrapper you need to get them out of the function, as shown in this question.

import inspect
from functools import wraps

def get_default_args(func):
    signature = inspect.signature(func)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
            print('Calling decorated function')
            print('args:', args)
            kwargs = get_default_args(f)
            kwargs.update(kwds)
            print('kwargs:', kwargs)
            return f(*args, **kwds)
    return wrapper

@my_decorator
def example(i, j=0):
    """Docstring"""
    print('Called example function')

example(i=1)

Output:

Calling decorated function
args: ()
kwargs: {'i': 1, 'j': 0}
Called example function
Isogamete answered 5/1, 2019 at 19:55 Comment(3)
Thanks for the answer, great idea! For others who find this, it's worth noting that a dict.update() will overwrite any keyword parameters with the default value. You'll need to roll your own dict update with no overwriting to avoid this behavior.Chongchoo
Calling example(1) gives kwargs: {'j': 0} with the i key missing, which might be a problem.Sailesh
@Sailesh Lucas Wiman's answer solves the problem.Arum
P
9

Getting the exact list of args and kwargs is a little tricky, since you can pass positional args as a kwarg, or vice versa. Newer versions of python also add positional-only or keyword only arguments.

However, inspect.signature has a mechanism which can apply defaults: calling .bind(*args, **kwargs) followed by .apply_defaults(). This can give you a dictionary of effectively what all the arguments are to the function. In the example in OP, this becomes:

from functools import wraps
import inspect
def my_decorator(f):
    sig = inspect.signature(f)
    @wraps(f)
    def wrapper(*args, **kwds):
        bound = sig.bind(*args, **kwds)
        bound.apply_defaults()
        print('Calling decorated function')
        print('called with:', bound.arguments)
        return f(*args, **kwds)
    return wrapper

@my_decorator
def example(i, j=0):
    """Docstring"""
    print('Called example function')

example(i=1)

This outputs the following on Python 3.9:

Calling decorated function
called with: OrderedDict([('i', 1), ('j', 0)])
Called example function
Porscheporsena answered 14/9, 2021 at 0:24 Comment(2)
This is the only answer that works in all cases. All solutions relying on *args, **kwargs, and dict comprehension will overlook arguments that are not explicitly passed but rely on dict comprehension.Arum
This solution works for all cases, should be flagged as answer. Thank you! This is a very clean solution =)Abrogate
V
4

You can get default argument values by using __defaults__ special attribute.

def my_decorator(f):
@wraps(f)
def wrapper(*args, **kwds):
    print('def args values', f.__defaults__)
    return f(*args, **kwds)
return wrapper

Reference: look for __defaults__ in https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy

A tuple containing default argument values for those arguments that have defaults, or None if no arguments have a default value

Valladolid answered 30/7, 2015 at 18:2 Comment(1)
This returns a tuple of the default values of the arguments but it doesn't return the names of the arguments.Alcott

© 2022 - 2024 — McMap. All rights reserved.