Reputation: 2739
Suppose I have some function, f
:
def f (a=None):
print a
Now, if I have a dictionary such as dct = {"a":"Foo"}
, I may call f(**dct)
and get the result Foo
printed.
However, suppose I have a dictionary dct2 = {"a":"Foo", "b":"Bar"}
. If I call f(**dct2)
I get a
TypeError: f() got an unexpected keyword argument 'b'
Fair enough. However, is there anyway to, in the definition of f
or in the calling of it, tell Python to just ignore any keys that are not parameter names? Preferable a method that allows defaults to be specified.
Upvotes: 80
Views: 59378
Reputation: 2255
[@Aviendha's answer][1] is great. Based on their post, I wrote an enhanced version supporting the default value in function's keywords-arguments signature, in Python 3.6:
import inspect
from inspect import Parameter
import functools
from typing import Callable, Any
def ignore_unexpected_kwargs(func: Callable[..., Any]) -> Callable[..., Any]:
def filter_kwargs(kwargs: dict) -> dict:
sig = inspect.signature(func)
# Parameter.VAR_KEYWORD - a dict of keyword arguments that aren't bound to any other
if any(map(lambda p: p.kind == Parameter.VAR_KEYWORD, sig.parameters.values())):
# if **kwargs exist, return directly
return kwargs
_params = list(filter(lambda p: p.kind in {Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD},
sig.parameters.values()))
res_kwargs = {
param.name: kwargs[param.name]
for param in _params if param.name in kwargs
}
return res_kwargs
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
kwargs = filter_kwargs(kwargs)
return func(*args, **kwargs)
return wrapper
if __name__ == "__main__":
@ignore_unexpected_kwargs
def foo(a, b=0, c=3):
return a, b, c
assert foo(0, 0, 0) == (0, 0, 0)
assert foo(a=1, b=2, c=3) == (1, 2, 3)
dct = {'a': 1, 'b': 2, 'd': 4}
assert foo(**dct) == (1, 2, 3)
@ignore_unexpected_kwargs
def fuzz(*args):
return sum(args)
# testing will not change original logic
assert fuzz(1, 2, 3) == 6
@ignore_unexpected_kwargs
def bar(a, b, **kwargs):
return a, b, kwargs.get('c', 3), kwargs.get('d', 4)
assert bar(**{'a': 1, 'b': 2, 'd': 4, 'e': 5}) == (1, 2, 3, 4)
Upvotes: 1
Reputation: 1463
I addressed some points in @Menglong Li's answer and simplified the code.
import inspect
import functools
def ignore_unmatched_kwargs(f):
"""Make function ignore unmatched kwargs.
If the function already has the catch all **kwargs, do nothing.
"""
if contains_var_kwarg(f):
return f
@functools.wraps(f)
def inner(*args, **kwargs):
filtered_kwargs = {
key: value
for key, value in kwargs.items()
if is_kwarg_of(key, f)
}
return f(*args, **filtered_kwargs)
return inner
def contains_var_kwarg(f):
return any(
param.kind == inspect.Parameter.VAR_KEYWORD
for param in inspect.signature(f).parameters.values()
)
def is_kwarg_of(key, f):
param = inspect.signature(f).parameters.get(key, False)
return param and (
param.kind is inspect.Parameter.KEYWORD_ONLY or
param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
)
Here are some test cases:
@ignore_unmatched_kwargs
def positional_or_keywords(x, y):
return x, y
@ignore_unmatched_kwargs
def keyword_with_default(x, y, z = True):
return x, y, z
@ignore_unmatched_kwargs
def variable_length(x, y, *args, **kwargs):
return x, y, args,kwargs
@ignore_unmatched_kwargs
def keyword_only(x, *, y):
return x, y
# these test should run without error
print(
positional_or_keywords(x = 3, y = 5, z = 10),
positional_or_keywords(3, y = 5),
positional_or_keywords(3, 5),
positional_or_keywords(3, 5, z = 10),
keyword_with_default(2, 2),
keyword_with_default(2, 2, z = False),
keyword_with_default(2, 2, False),
variable_length(2, 3, 5, 6, z = 3),
keyword_only(1, y = 3),
sep='\n'
)
# these test should raise an error
print(
#positional_or_keywords(3, 5, 6, 4),
#keyword_with_default(2, 2, 3, z = False),
#keyword_only(1, 2),
sep = '\n'
)
Upvotes: 4
Reputation: 373
I used Aviendha's idea to build my own. It is only tested for a very simple case but it might be useful for some people:
import inspect
def filter_dict(func, kwarg_dict):
sign = inspect.signature(func).parameters.values()
sign = set([val.name for val in sign])
common_args = sign.intersection(kwarg_dict.keys())
filtered_dict = {key: kwarg_dict[key] for key in common_args}
return filtered_dict
Tested on this specific case:
def my_sum(first, second, opt=3):
return first + second - opt
a = {'first': 1, 'second': 2, 'third': 3}
new_kwargs = filter_dict(my_sum, a)
The example returns new_args = {'first': 1, 'second': 2}
which can then be passed to my_sum
as my_sum(**new_args)
Upvotes: 2
Reputation: 63767
As an extension to the answer posted by @Bas, I would suggest to add the kwargs arguments (variable length keyword arguments) as the second parameter to the function
>>> def f (a=None, **kwargs):
print a
>>> dct2 = {"a":"Foo", "b":"Bar"}
>>> f(**dct2)
Foo
This would necessarily suffice the case of
Upvotes: 65
Reputation: 783
If you cannot change the function definition to take unspecified **kwargs, you can filter the dictionary you pass in by the keyword arguments using the argspec function in older versions of python or the signature inspection method in Python 3.6.
import inspect
def filter_dict(dict_to_filter, thing_with_kwargs):
sig = inspect.signature(thing_with_kwargs)
filter_keys = [param.name for param in sig.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD]
filtered_dict = {filter_key:dict_to_filter[filter_key] for filter_key in filter_keys}
return filtered_dict
def myfunc(x=0):
print(x)
mydict = {'x':2, 'y':3}
filtered_dict = filter_dict(mydict, myfunc)
myfunc(**filtered_dict) # 2
myfunc(x=3) # 3
Upvotes: 32
Reputation: 18488
This can be done by using **kwargs
, which allows you to collect all undefined keyword arguments in a dict:
def f(**kwargs):
print kwargs['a']
Quick test:
In [2]: f(a=13, b=55)
13
EDIT If you still want to use default arguments, you keep the original argument with default value, but you just add the **kwargs
to absorb all other arguments:
In [3]: def f(a='default_a', **kwargs):
...: print a
...:
In [4]: f(b=44, a=12)
12
In [5]: f(c=33)
default_a
Upvotes: 22