Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call a function with a dictionary that contains more items than the function has parameters?

I am looking for the best way to combine a function with a dictionary that contains more items than the function's inputs

basic **kwarg unpacking fails in this case:

def foo(a,b):
    return a + b

d = {'a':1,
     'b':2,
     'c':3}

foo(**d)
--> TypeError: foo() got an unexpected keyword argument 'c'

After some research I came up with the following approach:

import inspect

# utilities
def get_input_names(function):
    '''get arguments names from function'''
    return inspect.getargspec(function)[0]

def filter_dict(dict_,keys):
    return {k:dict_[k] for k in keys}

def combine(function,dict_):
    '''combine a function with a dictionary that may contain more items than the function's inputs '''
    filtered_dict = filter_dict(dict_,get_input_names(function))
    return function(**filtered_dict)

# examples
def foo(a,b):
    return a + b

d = {'a':1,
     'b':2,
     'c':3}

print combine(foo,d)
--> 3

My question is: is this a good way of dealing with this problem, or is there a better practice or is there a mechanism in the language that I'm missing perhaps?

like image 929
Len Blokken Avatar asked Mar 19 '16 13:03

Len Blokken


People also ask

How do you pass multiple parameters to a function?

Note that when you are working with multiple parameters, the function call must have the same number of arguments as there are parameters, and the arguments must be passed in the same order.

How do you pass a dictionary item as a function argument in Python?

Passing Dictionary as kwargs “ kwargs ” stands for keyword arguments. It is used for passing advanced data objects like dictionaries to a function because in such functions one doesn't have a clue about the number of arguments, hence data passed is be dealt properly by adding “**” to the passing type.

How do you pass multiple dictionary parameters in Python?

Use *args to take multiple arguments. Show activity on this post. Show activity on this post. If you want to avoid combining the dictionaries, then provide them as arguments rather than keyword arguments, and set their defaults to something (such as None ), and convert None to {} and otherwise copy the dictionary.


5 Answers

How about making a decorator that would filter allowed keyword arguments only:

import inspect


def get_input_names(function):
    '''get arguments names from function'''
    return inspect.getargspec(function)[0]


def filter_dict(dict_,keys):
    return {k:dict_[k] for k in keys}


def filter_kwargs(func):
   def func_wrapper(**kwargs):
       return func(**filter_dict(kwargs, get_input_names(func)))
   return func_wrapper


@filter_kwargs
def foo(a,b):
    return a + b


d = {'a':1,
     'b':2,
     'c':3}

print(foo(**d))

What is nice about this decorator is that it is generic and reusable. And you would not need to change the way you call and use your target functions.

like image 152
alecxe Avatar answered Oct 23 '22 08:10

alecxe


All of these answers are wrong.

It is not possible to do what you are asking, because the function might be declared like this:

def foo(**kwargs):
    a = kwargs.pop('a')
    b = kwargs.pop('b')
    if kwargs:
        raise TypeError('Unexpected arguments: %r' % kwargs)

Now, why on earth would anyone write that?

Because they don't know all of the arguments ahead of time. Here's a more realistic case:

def __init__(self, **kwargs):
    for name in self.known_arguments():
        value = kwargs.pop(name, default)
        self.do_something(name, value)
    super().__init__(**kwargs)  # The superclass does not take any arguments

And here is some real-world code which actually does this.

You might ask why we need the last line. Why pass arguments to a superclass that doesn't take any? Cooperative multiple inheritance. If my class gets an argument it does not recognize, it should not swallow that argument, nor should it error out. It should pass the argument up the chain so that another class I might not know about can handle it. And if nobody handles it, then object.__init__() will provide an appropriate error message. Unfortunately, the other answers will not handle that gracefully. They will see **kwargs and either pass no arguments or pass all of them, which are both incorrect.

The bottom line: There is no general way to discover whether a function call is legal without actually making that function call. inspect is a crude approximation, and entirely falls apart in the face of variadic functions. Variadic does not mean "pass whatever you like"; it means "the rules are too complex to express in a signature." As a result, while it may be possible in many cases to do what you're trying to do, there will always be situations where there is no correct answer.

like image 20
Kevin Avatar answered Oct 23 '22 06:10

Kevin


Your problem lies with the way you defined your function, it should be defined like this -

def foo(**kwargs):

And then inside the function you can iterate over the number of arguments sent to the function like so -

if kwargs is not None:
        for key, value in kwargs.iteritems():
                do something

You can find more info about using **kwargs in this post - http://pythontips.com/2013/08/04/args-and-kwargs-in-python-explained/

like image 11
Yaron Idan Avatar answered Oct 23 '22 07:10

Yaron Idan


You can also use a decorator function to filter out those keyword arguments that are not allowed in you function. Of you use the signature function new in 3.3 to return your function Signature

from inspect import signature
from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        sig = signature(func)
        result = func(*[kwargs[param] for param in sig.parameters])
        return result
    return wrapper

From Python 3.0 you can use getargspec which is deprecated since version 3.0

import inspect


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        argspec = inspect.getargspec(func).args
        result = func(*[kwargs[param] for param in argspec])
            return result
    return wrapper

To apply your decorate an existing function you need to pass your function as argument to your decorator:

Demo:

>>> def foo(a, b):
...     return a + b
... 
>>> foo = decorator(foo)
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> foo(**d)
3

To apply your decorator to a newly function simply use @

>>> @decorator
... def foo(a, b):
...     return a + b
... 
>>> foo(**d)
3

You can also define your function using arbitrary keywords arguments **kwargs.

>>> def foo(**kwargs):
...     if 'a' in kwargs and 'b' in kwargs:
...         return kwargs['a'] + kwargs['b']
... 
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> foo(**d)
3
like image 8
styvane Avatar answered Oct 23 '22 08:10

styvane


I would do something like this:

def combine(function, dictionary):
    return function(**{key:value for key, value in dictionary.items()
                    if key in inspect.getargspec(function)[0]}
    )

Use:

>>> def this(a, b, c=5):
...     print(a, b, c)
...
>>> combine(this, {'a': 4, 'b': 6, 'c': 6, 'd': 8})
4 6 6
>>> combine(this, {'a': 6, 'b': 5, 'd': 8})
6 5 5
like image 3
zondo Avatar answered Oct 23 '22 08:10

zondo