Give function defaults arguments from a dictionary in Python

后端 未结 8 2269
予麋鹿
予麋鹿 2021-01-01 18:26

Let\'s imagine I have a dict :

d = {\'a\': 3, \'b\':4}

I want to create a function f that does the exact same thing than this function :

相关标签:
8条回答
  • 2021-01-01 18:32

    This question is very interesting, and it seemed different people have their different own guess about what the question really want.

    I have my own too. Here is my code, which can express myself:

    # python3 only
    from collections import defaultdict
    
    # only set once when function definition is executed
    def kwdefault_decorator(default_dict):
        def wrapper(f):
            f.__kwdefaults__ = {}
            f_code = f.__code__
            po_arg_count = f_code.co_argcount
            keys = f_code.co_varnames[po_arg_count : po_arg_count + f_code.co_kwonlyargcount]
            for k in keys:
                f.__kwdefaults__[k] = default_dict[k]
            return f
    
        return wrapper
    
    default_dict = defaultdict(lambda: "default_value")
    default_dict["a"] = "a"
    default_dict["m"] = "m"
    
    @kwdefault_decorator(default_dict)
    def foo(x, *, a, b):
        foo_local = "foo"
        print(x, a, b, foo_local)
    
    @kwdefault_decorator(default_dict)
    def bar(x, *, m, n):
        bar_local = "bar"
        print(x, m, n, bar_local)
    
    foo(1)
    bar(1)
    # only kw_arg permitted
    foo(1, a=100, b=100)
    bar(1, m=100, n=100)
    

    output:

    1 a default_value
    1 m default_value
    1 100 100
    1 100 100
    
    0 讨论(0)
  • 2021-01-01 18:33

    Python is designed such that the local variables of any function can be determined unambiguously by looking at the source code of the function. So your proposed syntax

    def f(x, **d=d):
      print(x, a, b)
    

    is a nonstarter because there's nothing that indicates whether a and b are local to f or not; it depends on the runtime value of the dictionary, whose value could change across runs.

    If you can resign yourself to explicitly listing the names of all of your parameters, you can automatically set their default values at runtime; this has already been well covered in other answers. Listing the parameter names is probably good documentation anyway.

    If you really want to synthesize the whole parameter list at run time from the contents of d, you would have to build a string representation of the function definition and pass it to exec. This is how collections.namedtuple works, for example.

    Variables in module and class scopes are looked up dynamically, so this is technically valid:

    def f(x, **kwargs):
        class C:
            vars().update(kwargs)  # don't do this, please
            print(x, a, b)
    

    But please don't do it except in an IOPCC entry.

    0 讨论(0)
  • 2021-01-01 18:41

    Posting this as an answer because it would be too long for a comment.

    Be careful with this answer. If you try

    @kwargs_decorator(a='a', b='b')
    def f(x, a, b):
        print(f'x = {x}')
        print(f'a = {a}')
        print(f'b = {b}')
    
    f(1, 2)
    

    it will issue an error:

    TypeError: f() got multiple values for argument 'a'
    

    because you are defining a as a positional argument (equal to 2).

    I implemented a workaround, even though I'm not sure if this is the best solution:

    def default_kwargs(**default):
        from functools import wraps
        def decorator(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                from inspect import getfullargspec
                f_args = getfullargspec(f)[0]
                used_args = f_args[:len(args)]
    
                final_kwargs = {
                    key: value 
                    for key, value in {**default, **kwargs}.items() 
                    if key not in used_args
                }
    
                return f(*args, **final_kwargs)
            return wrapper
        return decorator
    

    In this solution, f_args is a list containing the names of all named positional arguments of f. Then used_args is the list of all parameters that have effectively been passed as positional arguments. Therefore final_kwargs is defined almost exactly like before, except that it checks if the argument (in the case above, a) was already passed as a positional argument.

    For instance, this solution works beautifully with functions such as the following.

    @default_kwargs(a='a', b='b', d='d')
    def f(x, a, b, *args, c='c', d='not d', **kwargs):
        print(f'x = {x}')
        print(f'a = {a}')
        print(f'b = {b}')
        for idx, arg in enumerate(args):
            print(f'arg{idx} = {arg}')
        print(f'c = {c}')
        for key, value in kwargs.items():
            print(f'{key} = {value}')
    
    f(1)
    f(1, 2)
    f(1, b=3)
    f(1, 2, 3, 4)
    f(1, 2, 3, 4, 5, c=6, g=7)
    

    Note also that the default values passed in default_kwargs have higher precedence than the ones defined in f. For example, the default value for d in this case is actually 'd' (defined in default_kwargs), and not 'not d' (defined in f).

    0 讨论(0)
  • 2021-01-01 18:46

    try this:

    # Store the default values in a dictionary
    >>> defaults = {
    ...     'a': 1,
    ...     'b': 2,
    ... }
    >>> def f(x, **kwa):
            # Each time the function is called, merge the default values and the provided arguments
            # For python >= 3.5:
            args = {**defaults, **kwa}
            # For python < 3.5:
            # Each time the function is called, copy the default values
            args = defaults.copy()
            # Merge the provided arguments into the copied default values
            args.update(kwa)
    ...     print(args)
    ... 
    >>> f(1, f=2)
    {'a': 1, 'b': 2, 'f': 2}
    >>> f(1, f=2, b=8)
    {'a': 1, 'b': 8, 'f': 2}
    >>> f(5, a=3)
    {'a': 3, 'b': 2}
    

    Thanks Olvin Roght for pointing out how to nicely merge dictionaries in python >= 3.5

    0 讨论(0)
  • 2021-01-01 18:47

    You cannot achieve this at function definition because Python determines the scope of a function statically. Although, it is possible to write a decorator to add in default keyword arguments.

    from functools import wraps
    
    def kwargs_decorator(dict_kwargs):
        def wrapper(f):
            @wraps(f)
            def inner_wrapper(*args, **kwargs):
                new_kwargs = {**dict_kwargs, **kwargs}
                return f(*args, **new_kwargs)
            return inner_wrapper
        return wrapper
    

    Usage

    @kwargs_decorator({'bar': 1})
    def foo(**kwargs):
        print(kwargs['bar'])
    
    foo() # prints 1
    

    Or alternatively if you know the variable names but not their default values...

    @kwargs_decorator({'bar': 1})
    def foo(bar):
        print(bar)
    
    foo() # prints 1
    

    Caveat

    The above can be used, by example, to dynamically generate multiple functions with different default arguments. Although, if the parameters you want to pass are the same for every function, it would be simpler and more idiomatic to simply pass in a dict of parameters.

    0 讨论(0)
  • 2021-01-01 18:48

    How about the **kwargs trick?

    def function(arg0, **kwargs):
        print("arg is", arg0, "a is", kwargs["a"], "b is", kwargs["b"])
    
    d = {"a":1, "b":2}
    function(0., **d)
    

    outcome:

    arg is 0.0 a is 1 b is 2
    
    0 讨论(0)
提交回复
热议问题