如何使用包含比函数参数更多项的字典调用函数?

26

我正在寻找将一个函数与一个包含比函数输入更多项的字典结合的最佳方法。

在这种情况下,基本的关键字参数解包失败:

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

d = {'a':1,
     'b':2,
     'c':3}

foo(**d)
--> TypeError: foo() got an unexpected keyword argument 'c'

经过一些研究,我想出了以下方法:

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

我的问题是:这是处理这个问题的好方法吗,还是有更好的做法或者可能是我所缺失的语言机制?

6个回答

24

那么如何编写一个装饰器仅过滤允许使用的关键字参数呢:

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

这个装饰器的好处在于它是通用且可重复使用的。而且你不需要改变调用和使用目标函数的方式。


15

所有这些答案都是错误的。

你所要求的是不可能实现的,因为该函数可能像这样声明:

def foo(**kwargs):
    a = kwargs.pop('a')
    b = kwargs.pop('b')
    if kwargs:
        raise TypeError('Unexpected arguments: %r' % kwargs)

因为他们不知道所有的参数。下面是一个更加现实的示例:
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

这里是实际执行此操作的一些现实代码(此处)

你可能会问为什么需要最后一行。为什么要传递不需要任何参数的超类?合作多重继承。如果我的类收到一个它不认识的参数,它不应该吞噬该参数,也不应该出错。它应该将参数传递给上级,以便另一个我可能不知道的类可以处理它。如果没有人处理它,那么 object.__init__() 将提供适当的错误消息。不幸的是,其他答案不能优雅地处理它。他们会看到 **kwargs 并且要么不传递参数,要么传递所有参数,这两种情况都是不正确的。

底线:没有通用的方法来发现一个函数调用是否合法,而不实际进行该函数调用。 inspect 是一种粗略的近似,当面对可变参数函数时完全失效。 可变参数并不意味着“传递任何你喜欢的东西”;它意味着“规则过于复杂,无法在签名中表达”。 因此,虽然在许多情况下可能可以做到你试图做的事情,但总会有一些情况下没有正确答案。

1
谢谢Kevin!最终我接受了alecxe的答案,因为它对我的情况最直接有帮助,但我也发现你的回答非常有趣,我同意认识到有重要的场景需要考虑,其中“过滤kwargs”方法将不起作用。 - Len Blokken

11

你的问题在于你定义函数的方式,它应该像这样进行定义 -

def foo(**kwargs):

然后在函数内部,您可以像这样迭代发送到函数的参数数量 -

if kwargs is not None:
        for key, value in kwargs.iteritems():
                do something

您可以在此文章中找到有关使用**kwargs的更多信息 - http://pythontips.com/2013/08/04/args-and-kwargs-in-python-explained/


1
谢谢。也许我应该提一下,我正在为一个程序构建框架,其他人(有时是相对初学者)正在创建函数。因此,我希望他们不必处理 **kwargs,而是使用基本的函数输入:foo(a,b)。我想在实用函数中隐藏这个 **kwargs 过滤复杂性。 - Len Blokken

8
你可以使用一个装饰器函数来过滤掉那些在你的函数中不允许的关键字参数。如果你使用在3.3中新增的signature函数来返回你的函数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

自 Python 3.0 开始,您可以使用 getargspec,但此方法已自版本 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

要为现有函数添加装饰器,您需要将该函数作为参数传递给装饰器:
演示:
>>> def foo(a, b):
...     return a + b
... 
>>> foo = decorator(foo)
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> foo(**d)
3

要将装饰器应用于新函数,只需使用@

>>> @decorator
... def foo(a, b):
...     return a + b
... 
>>> foo(**d)
3

您还可以使用任意关键字参数**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

3
我会这样做:
def combine(function, dictionary):
    return function(**{key:value for key, value in dictionary.items()
                    if key in inspect.getargspec(function)[0]}
    )

使用:

>>> 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

2

这仍然是修改原始函数,但您可以在参数列表末尾创建一个kwargs bitbucket:

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

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

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