thothal
thothal

Reputation: 20329

Call a function with a dict as parameter

I am currently working on a command line interface with argparse. I receive the parsed arguments as a Namespace object from arg_parse. Basically the arguments coming in form the CLI should be passed down to 2 functions. Now I am looking for a smart way to split the Namespace such that I can simply use unpacking.

The following example should illustrate what I would need:

import argparse
ns1 = argparse.Namespace(foo=1)
ns2 = argparse.Namespace(bar=2)
ns3 = argparse.Namespace(foo=1, bar=2)

def f(foo): return foo
def g(bar): return 10 * bar

f(**vars(ns1)) # works
g(**vars(ns2)) # works
f(**vars(ns3)) # does not work, 'bar' is not an argument of f

I could hard code the filter like in

f_args = ["foo"]
g_args = ["bar"]

f(**{k:v for k, v in vars(ns3).items() if k in f_args}) # works
g(**{k:v for k, v in vars(ns3).items() if k in g_args}) # works

But this feels error prone (if the signature of f will change, I have to remember to change f_args as well.). I could however use inspect.getfullargspec to automatize this part like in:

import inspect
f_args = inspect.getfullargspec(f).args

But this all feels hackish to me and I was wondering whether I am not overlooking an easy pattern for this?


Update

As pointed out by @doer_uvc one approach would be to treat my functions with a "catch all" parameter like in:

def f(foo, **kwargs): return foo

While this is a viable solution, I would be curious to find solutions where I do not need to touch the signature of f itself.

Upvotes: 1

Views: 190

Answers (2)

Utsav Chokshi
Utsav Chokshi

Reputation: 1395

If writing functions are within your scope then make your functions accept any number of keyword arguments and check for arguments of interest within function.

class MissingArgumentError(ValueError):
    pass


def f(**kwargs):
    if not 'foo' in kwargs:
        raise MissingArgumentError('Missing keyword argument `foo`')
    return kwargs['foo']


def g(**kwargs):
    if not 'bar' in kwargs:
       raise MissingArgumentError('Missing keyword argument `bar`')
    return 10 * kwargs['bar']

In this method, even if your function signature changes, nothing else needs to be changed.


If you have to write multiple such functions then rather than repeating logic of argument checking, you can use decorators.

def argverifier(compulsory_args:list):
    def actual_decorator(function):
        def inner(**kwargs):
            for arg in compulsory_args:
                if arg not in kwargs:
                    raise MissingArgumentError(f'Missing keyword argument : {arg}')
            return function(**kwargs)
        return inner
    return actual_decorator

@argverifier(compulsory_args=['foo'])
def f(**kwargs): return kwargs['foo']

@argverifier(compulsory_args=['bar'])
def g(**kwargs): return 10 * kwargs['bar']

If you do not wish to change signature of a function then the viable solution is to write a function that extracts arguments using inspect module as you suggested.

from functools import partial
import inspect

def f(foo): return foo
def g(bar): return 10 * bar

def argextractor(args, kwargs):
    return {k:v for k, v in kwargs.items() if k in args}

f_argextractor = partial(argextractor, inspect.getfullargspec(f).args) 
g_argextractor = partial(argextractor, inspect.getfullargspec(g).args)

f(**f_argextractor(vars(ns3)))
g(**g_argextractor(vars(ns3)))

Upvotes: 1

MisterMiyagi
MisterMiyagi

Reputation: 50116

There is no shorthand for this. However, one can easily build this using inspect.signature – these are not hacks, Python signatures can get very complicated and inspect exists for just such purposes.

import inspect


def apply(call: 'Callable', kwargs: 'Dict[str, Any]'):
    """Apply all appropriate ``kwargs`` to ``call``"""
    call_kwargs = inspect.signature(call).parameters.keys()
    matching_kwargs = {name: kwargs[name] for name in kwargs.keys() & call_kwargs}
    return call(**matching_kwargs)

This inspects the parameters expected by call, and picks those supplied by kwargs. If kwargs is missing any parameters to call, the standard exception of missing parameters is thrown.

>>> def f(foo):
...     return foo
...
>>> apply(f, {'foo': 2, 'bar': 3})
2
>>> apply(f, {'baz': 2, 'bar': 3})
…
TypeError: f() missing 1 required positional argument: 'foo'

Upvotes: 2

Related Questions