如何忽略传递给函数的意外关键字参数?

78

假设我有一个函数,f:

def f (a=None):
    print a
现在,如果我有这样一个字典 dct = {"a":"Foo"},我可以调用f(**dct)并打印出结果 Foo
然而,假设我有一个字典 dct2 = {"a":"Foo", "b":"Bar"}。如果我调用f(**dct2),我会得到
TypeError: f() got an unexpected keyword argument 'b'

好的。但是,有没有办法在f的定义或调用中告诉Python忽略任何不是参数名的键?最好是能够指定默认值的方法。

6个回答

61

作为对@Bas发布的答案的扩展,我建议将kwargs参数(可变长度关键字参数)作为函数的第二个参数添加进去。

>>> def f (a=None, **kwargs):
    print a


>>> dct2 = {"a":"Foo", "b":"Bar"}
>>> f(**dct2)
Foo

这将必要地满足以下情况:

  1. 只需忽略不是参数名称的任何键即可
  2. 然而,它缺少参数的默认值,这是一个很好的功能,希望能够保留

2
我认为这就是原帖作者所寻找的内容。 - Noob Saibot
还要注意,f(10, b=20) 在这里会打印出 10,如果你只使用 **kwargs 是不正确的。 - abarnert
12
但如果该函数是由其他人(例如一个开源库)编写的,我就无法这样做。 - Aminah Nuraini
我不确定为什么答案说“它缺少参数的默认值”。在示例中使用了a=None。另外还有一个替代版本是:使用**_代替**kwargs - undefined

31

如果您无法更改函数定义以接受未指定的 **kwargs,请使用旧版Python中的argspec函数或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

1
这种方法适用于在别人编写了函数规范并且您无法从调用代码中更改它的情况下。好建议。id="dmid://uu023filterdict1623010139" - dreftymac
这似乎也是处理Numba函数的方式,其中不支持**kwargs - mcsoini
如果myfunc的签名为(*args, **kwargs),并且它被一个函数包装调用,那么这种方法就不起作用了。 - John Jiang
请注意,根据thing_with_kwargs的具体情况,您可能需要或不需要应用param.kind == param.POSITIONAL_OR_KEYWORD过滤器。如果thing_with_kwargs具有一些仅限关键字的参数,如果应用此过滤器,您将丢失这些参数。 - undefined

22

可以通过使用**kwargs来实现,这样您可以将所有未定义的关键字参数收集到一个dict中:

def f(**kwargs):
    print kwargs['a']

快速测试:

In [2]: f(a=13, b=55)
13

编辑 如果您仍然想使用默认参数,则保留具有默认值的原始参数,但只需添加**kwargs以吸收所有其他参数:

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

1
编辑有点误导性。你可以用第二个函数做的最重要的事情是将a作为一个位置参数传递,而第一个函数则不能。除非你想同时使用*args**kwargs并自己复制整个参数绑定机制。另一方面,处理默认值是微不足道的——print kwargs.get('a', 'default_a') - abarnert
实际上,使用kwargs.get()也是定义默认值的另一种方式。我解决方案的好处在于,你可以从函数头部就看到函数有一个默认值,而不必深入函数体中查找。现在它还接受位置参数,这只是一些小的副作用。 - Bas Swinckels
我认为API的影响比API的内省能力要大得多。 - abarnert

4

我在@Menglong Li的答案中提到了一些问题并简化了代码。

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
    )

以下是一些测试用例:

@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'
)

我喜欢你的回答,但字典推导式不够简洁易读。字典推导式应该尽量简短易读。我建议将 inspect.signature(f).parameters.items() 移到一个变量中,并将复杂的 if 条件放入自己的函数中。这将使推导式更容易阅读。 - Neuron
1
@Neuron 感谢!我重构了代码,现在filtered_kwargs变量确实只包含来自kwargs的满足某些条件的键值对,该条件已移至单独的函数。 - Apiwat Chantawibul
非常漂亮! :) - Neuron

2

我使用了Aviendha的想法来构建我的网站。虽然只测试了一种非常简单的情况,但对于一些人可能会有用:

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

在这个特定案例中进行了测试:

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)


该示例返回new_args = {'first': 1, 'second': 2},然后可以将其作为my_sum(**new_args)传递给my_sum函数。

1

[@Aviendha的回答][1]非常好。根据他们的帖子,我编写了一个增强版,支持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)

1
我从这个答案中学到了很多,但它仍然存在问题。例如,[1] 它无法处理 run(1,2),因为 wrapper 不接受任何位置参数,即使原始的 def run 声明了位置参数。[2] 不能处理 Python 3 中新引入的 KEYWORD_ONLY。[3] 无法识别原始函数是否已经有 **kwargs。 - Apiwat Chantawibul
如果同时以位置和关键字形式提供值,在这种情况下,我会让Python标准库决定应该引发什么错误。测试用例是我的答案中的keyword_with_default(2,2,3,z=False) - Apiwat Chantawibul
@Apiwat Chantawibul,我修改了代码,使其可以像run(1,2)一样调用。 - Menglong Li

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接