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 dict
s 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!
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}