【问题标题】: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 忽略任何不是参数名称的键?首选允许指定默认值的方法。

【问题讨论】:

    标签: python function dictionary


    【解决方案1】:

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

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

    这必然足以满足

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

    【讨论】:

    • 我认为这就是 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)
        猜你喜欢
        • 2020-03-08
        • 2019-07-07
        • 2018-03-21
        • 2020-01-21
        • 2021-02-22
        • 2017-08-01
        • 1970-01-01
        • 2020-03-15
        相关资源
        最近更新 更多