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

Posted

技术标签:

【中文标题】如何忽略传递给函数的意外关键字参数?【英文标题】:How does one ignore unexpected keyword arguments passed to a function? 【发布时间】:2014-12-18 09:12:22 【问题描述】:

假设我有一些功能,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 忽略任何不是参数名称的键?首选允许指定默认值的方法。

【问题讨论】:

【参考方案1】:

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

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


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

这必然足以满足

的情况
    忽略任何不是参数名称的键 但是,它缺少参数的默认值,这是一个很好的功能,如果保留会很好

【讨论】:

我认为这就是 OP 正在寻找的。​​span> 另请注意f(10, b=20) 将在此处打印10,如果您仅使用**kwargs,则不会出现这种情况。 但是如果这个函数是别人做的(比如开源库),我就做不到【参考方案2】:

如果您无法更改函数定义以采用未指定的 **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

【讨论】:

这种方法有助于在其他人编写函数规范并且您无法从调用代码更改它的情况下。很好的建议。 id="dmid://uu023filterdict1623010139"【参考方案3】:

这可以通过使用**kwargs 来完成,它允许您在一个字典中收集所有未定义的关键字参数:

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

【讨论】:

编辑有点误导。你可以用第二个做的最重要的事情是你不能用第一个做的就是将a 作为一个位置参数。 (除非您想使用 *args**kwargs 并自己重现整个参数绑定机制。)另一方面,仅处理默认值是微不足道的——print kwargs.get('a', 'default_a') 确实,使用kwargs.get() 是定义默认值的另一种方式。我的解决方案的好处是,您可以仅从函数头中看到该函数具有默认值,而无需深入研究函数的主体。它现在也需要位置参数是一些小的副作用。 我认为 API 的影响远大于该 API 的自省性。【参考方案4】:

我使用Aviendha's 的想法来构建我自己的想法。它仅针对一个非常简单的案例进行了测试,但它可能对某些人有用:

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

【讨论】:

【参考方案5】:

我解决了@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 any(param.kind == inspect.Parameter.VAR_KEYWORD for param in inspect.signature(f).parameters.values()):
        return f
    #
    @functools.wraps(f)
    def inner(*args,**kwargs):
        # For each keyword arguments recognised by f,
        # take their binding from **kwargs received
        filtered_kwargs = 
            name:kwargs[name]
            for name,param in inspect.signature(f).parameters.items() if (
                param.kind is inspect.Parameter.KEYWORD_ONLY or
                param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
            ) and
            name in kwargs
        
        return f(*args,**filtered_kwargs)
    return inner

这里有一些广泛的测试用例:

@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 error
print(
    #positional_or_keywords(3,5,6,4),
    #keyword_with_default(2,2,3,z=False),
    #keyword_only(1,2),
    sep='\n'
)

【讨论】:

【参考方案6】:

@Aviendha 的回答很棒,根据她的帖子,在 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]:
    sig = inspect.signature(func)
    params = sig.parameters.values()

    def filter_kwargs(kwargs: dict) -> dict:
        _params = filter(lambda p: p.kind in Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY, params)

        res_kwargs = 
            param.name: kwargs[param.name]
            for param in _params if param.name in kwargs
        
        return res_kwargs

    def contain_var_keyword() -> bool:
        return len(params) >= 1 and any(filter(lambda p: p.kind == Parameter.VAR_KEYWORD, params))

    def contain_var_positional() -> bool:
        return len(params) >= 1 and any(filter(lambda p: p.kind == Parameter.VAR_POSITIONAL, params))

    @functools.wraps(func)
    def wrapper(*args, **kwargs) -> Any:
        kwargs = filter_kwargs(kwargs)
        return func(*args, **kwargs)

    ret_func = func
    if not contain_var_keyword():
        if contain_var_positional():
            raise RuntimeError('*args not supported')
        ret_func = wrapper

    return ret_func


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)
    dct = 'a': 1, 'b': 2
    assert foo(**dct) == (1, 2, 3)


    @ignore_unexpected_kwargs
    def bar(a, b, **kwargs):
        return a, b, kwargs.get('c', 3)


    assert bar(**'a': 1, 'b': 2, 'c': 4) == (1, 2, 4)

【讨论】:

我从这个答案中学到了很多,但它仍然存在问题。例如,[1] 它无法处理run(1,2),因为wrapper 不接受任何位置参数,即使原始def run 声明了位置参数。 [2] 不处理 python 3 中新的 KEYWORD_ONLY [3] 不识别原始函数是否已经具有 **kwargs。 在以位置和关键字形式提供值的情况下,我让 Python 标准来决定它应该引发什么错误。在我的回答中,测试用例是keyword_with_default(2,2,3,z=False)

以上是关于如何忽略传递给函数的意外关键字参数?的主要内容,如果未能解决你的问题,请参考以下文章

给函数传递不定关键字的参数 和

创建函数-----------(在函数中使用变量向函数传递参数在函数中处理变量关键字local)

函数名传递与名称空间

Python函数-参数传递

如何将关键字参数作为参数传递给函数?

如何使用 `type()` 内置函数传递类关键字参数?