Len Blokken
Len Blokken

Reputation: 303

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?

Upvotes: 26

Views: 1304

Answers (6)

Sede
Sede

Reputation: 61253

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

Upvotes: 8

Christian Mann
Christian Mann

Reputation: 8125

This is still modifying the original function, but you can create a kwargs bitbucket at the end of the argument list:

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

foo(**{
    'a': 5,
    'b': 8,
    'c': '🐘'
}) # 13

Upvotes: 2

Kevin
Kevin

Reputation: 30161

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.

Upvotes: 15

alecxe
alecxe

Reputation: 474001

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.

Upvotes: 24

zondo
zondo

Reputation: 20346

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

Upvotes: 3

Yaron Idan
Yaron Idan

Reputation: 6765

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/

Upvotes: 11

Related Questions