Search code examples
pythonfunctionarguments

How to deal with multiple keyword arguments?


I've got some trouble dealing with defining a user friendly function interface when passing two keyworded arguments with the same key.

Question

What is the best way to make it possible to call a function where two keyworded arguments have the same key and the second keyworded argument has precedence?
If this problem occurs, the first keyworded argument always stems from an unzipped database in a dict, while the second keyworded argument is always passed by giving it "directly" as a keyworded argument.
The database dictionary values must not be overwritten in the outer scopy of the functions, since they may be used multiple times.
edit: To keep up the usability of the function for the user, a backend-implementation is preferred. This means that the user can simply pass arguments to the function without the use of additional modules, while the function itself does all the magic.


Problem

I've got a function, called fun_one here, which receives a multitude of arguments defined directly by the user of my program. This may be length and width of a heat exchanger for example. To ease the use of the function and make the calling code as short as possible, the use of databases is encouraged. These databases contain the data in a dict (or in a pandas Series), in this case called inputs.
To pass the database-dict inputs to the function, it is unzipped with **inputs and thus passed as keyworded arguments.
Now if the user wants to overwrite a specific argument of the database, my understanding of a user-friendly approach would be to just let him pass the preceded argument again, for example with length=23.7, and internally overwrite the argument from the database. But of course (see example code) this raises the error before I can even enter the function where I could try/except:

TypeError: fun_one() got multiple values for keyword argument 'length'

Code example reproducing the error

def fun_one(*args, **kwargs):  # short example function
    print(kwargs)

inputs = {'length': 15.8, 'width': 1.1, 'some_other_args': np.random.rand(3)}

fun_one(**inputs, length=23.7)

My current solution

My current solution fun_two involves not unzipping the database and passing it to *args. It checks *args for dicts and sets values which are not yet in kwargs to kwargs, as shown in the code example below.

def fun_two(*args, **kwargs):  # example function printing kwargs
    print(kwargs)  # print kwargs before applying changes
    for arg in args:  # find dicts
        if type(arg) is dict:
            for key, val in arg.items():  # loop over dict
                _ = kwargs.setdefault(key, val)  # set val if key not in dict
    print(kwargs)  # print kwargs after applying changes

inputs = {'length': 15.8, 'width': 1.1, 'some_other_args': np.random.rand(3)}

fun_two(inputs, length=23.7)

But this approach is imho quite obscure for the user and requires looping and checking at the beginning of quite alot functions, since this will apply to numerous functions. (I'll outsource it to a module, so it is one line per function. But it still deviates from my understanding of an easy and clear function definition).

Is there any better (more Pythonic) way to do this? Did I oversee some way to do it in the process of calling the function? Performance does not matter.
Thanks in advance!


Solution

  • Easiest solution is using ChainMap from collections (manual pages). That way you can chose which arguments have precedence. Example:

    from collections import ChainMap
    
    def fun_one(*args, **kwargs):  # short example function
        print(kwargs)
    
    inputs = {'length': 15.8, 'width': 1.1, 'some_other_args': 1}
    
    c = ChainMap({'length': 23.7}, inputs)  # we overwrite length here
    fun_one(**c)
    

    Outputs:

    {'some_other_args': 1, 'width': 1.1, 'length': 23.7}
    

    If we call fun_one just with inputs:

    c = ChainMap(inputs)
    # or c = inputs
    fun_one(**c)
    

    Output will be:

    {'width': 1.1, 'length': 15.8, 'some_other_args': 1}
    

    From manual:

    A ChainMap groups multiple dicts or other mappings together to create a single, updateable view. If no maps are specified, a single empty dictionary is provided so that a new chain always has at least one mapping.

    You can wrap this ChainMap in decorator, one change is that don't call fun_one() with **input, only input:

    from collections import ChainMap
    
    def function_with_many_arguments(func):
        orig_func = func
        def _f(*args, **kwargs):
            if args:
                c = ChainMap(kwargs, args[0])
                return orig_func(**c)
            else:
                return orig_func(*args, **kwargs)
        return _f
    
    @function_with_many_arguments
    def fun_one(*args, **kwargs):  # short example function
        print(kwargs)
    
    inputs = {'length': 15.8, 'width': 1.1, 'some_other_args': 1}
    fun_one(inputs, length=23)
    

    Prints:

    {'some_other_args': 1, 'length': 23, 'width': 1.1}