Python将args转换为kwargs

34

我正在编写一个装饰器,它需要在装饰的函数被调用之前调用其他函数。 装饰的函数可能有位置参数,但是装饰器将要调用的函数只能接受关键字参数。 有没有一种方便的方法可以将位置参数转换为关键字参数?

我知道可以获取装饰函数的变量名称列表:

>>> def a(one, two=2):
...    pass

>>> a.func_code.co_varnames
('one', 'two')

但我无法弄清楚如何告诉哪些参数是按位置传递的,哪些是按关键字传递的。

我的装饰器看起来像这样:

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        hozer(**kwargs)
        self.f(*args, **kwargs)

除了比较kwargs和co_varnames之外,还有其他方法吗?我可以将不在kwargs中的任何内容添加到其中,然后抱着试试看的心态吗?


1
你为什么需要知道参数是位置参数? - Jason Coon
1
因为我需要将它们转换为kwargs以便在hozer函数中调用。这个函数只接受kwargs,但它需要知道最初调用的所有参数。所以根据人们是使用位置参数还是命名参数调用装饰函数,hozer函数可能会或可能不会获得所有所需的数据。 - jkoelker
7个回答

25

任何以位置参数形式传递的参数都会被传递给 *args。而任何以关键字参数形式传递的参数都会被传递给 **kwargs。 如果您拥有位置参数值和名称,则可以执行以下操作:

kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))

将它们全部转换为关键字参数。


4
实际上,kwargs.update(zip(myfunc.func_code.co_varnames, args)) 已经足够了。 dict.update 函数也可以处理二维迭代器。 - obskyr
1
如果函数myfunc中有默认参数,这个解决方案将无法完全填充kwargs - abulka
如果被装饰的函数是类成员,似乎这个方法不起作用(尽管实际上第一个位置参数的值为self,但self会被报告为第一个参数的名称)。 - xxbidiao
2
我不知道现在是否有人在使用inspect,但它在Python3中无法正常工作。 Copilot给了我这个代码:kwargs.update(zip(fn.__code__.co_varnames[1:], args))。顺便说一下,我的fn.__code__.co_varnames的第一个元素是 self - Pablo LION

22

如果您使用的是Python >= 2.7,则inspect.getcallargs()可以帮助您完成此操作。您只需将修饰后的函数作为第一个参数传递给它,然后将其余参数完全按照您计划调用的方式传递即可。例如:

>>> def f(p1, p2, k1=None, k2=None, **kwargs):
...     pass
>>> from inspect import getcallargs

我打算执行f('p1', 'p2', 'p3', k2='k2', extra='kx1')(请注意,k1作为p3的位置参数传递),因此...

>>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}

如果你知道被装饰的函数不会使用 **kwargs,那么这个键就不会出现在字典中,这样你就完成了(我假设没有 *args,因为这会违反一切都必须有名称的要求)。如果你确实需要 **kwargs,就像我在这个例子中所做的那样,并且想将它们与其余命名参数一起包含,那么需要再添加一行代码:

>>> call_args.update(call_args.pop('kwargs'))
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}
更新:对于Python版本>=3.3,请参见inspect.Signature.bind()和相关的inspect.signature函数,功能类似于(但更为健壮)inspect.getcallargs()

1
这是正确的方法(如果您有Python 2.7或更高版本,几乎每个人都有)。 - John Zwinck
请查看我基于getcallargs的完整答案。 - abulka

9
注意 - co_varnames将包括局部变量和关键字。这可能不重要,因为zip会截断较短的序列,但如果传递错误数量的参数,则可能导致混乱的错误消息。
您可以使用func_code.co_varnames [:func_code.co_argcount]避免此问题,但更好的方法是使用inspect模块。例如:
import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

您可能还需要处理函数定义了**kwargs*args的情况(即使只是在使用装饰器时引发异常)。如果设置了这些参数,则getargspec的第二个和第三个结果将返回它们的变量名称,否则它们将为None。


9

Nadia的回答是正确的,但我认为一个可工作的演示对那个回答很有用。

def decorator(func):
    def wrapped_func(*args, **kwargs):
        kwargs.update(zip(func.__code__.co_varnames, args))
        print(kwargs)
        return func(**kwargs)
    return wrapped_func

@decorator
def thing(a,b):
    return a+b

假设有这样一个被装饰过的函数,以下调用将会返回正确的答案:

thing(1, 2)  # prints {'a': 1, 'b': 2}  returns 3
thing(1, b=2)  # prints {'b': 2, 'a': 1}  returns 3
thing(a=1, b=2)  # prints {'a': 1, 'b': 2}  returns 3

请注意,如果您开始嵌套修饰器,事情就会变得很奇怪,因为装饰函数现在不再使用a和b,而是使用args和kwargs参数:
@decorator
@decorator
def thing(a,b):
    return a+b

这里thing(1,2)将会输出{'args': 1, 'kwargs': 2},并且产生一个TypeError: thing() got an unexpected keyword argument 'args'的错误。


如果被装饰的函数中有默认参数,那么Nadia的答案是不正确的。它无法完全填充kwargs,例如装饰def thing(a,b=5),然后调用thing(1),kwargs变成了{'a': 1}而不是{'a': 1, 'b': 5} - abulka

7

好的,这可能有些过度了。我为dectools包(在PyPi上)编写了它,因此您可以在那里获取更新。它返回考虑位置参数、关键字参数和默认参数的字典。该包中有一个测试套件(test_dict_as_called.py):

def _dict_as_called(function, args, kwargs):
    """ return a dict of all the args and kwargs as the keywords they would
    be received in a real function call.  It does not call function.
    """

    names, args_name, kwargs_name, defaults = inspect.getargspec(function)
    
    # assign basic args
    params = {}
    if args_name:
        basic_arg_count = len(names)
        params.update(zip(names[:], args))  # zip stops at shorter sequence
        params[args_name] = args[basic_arg_count:]
    else:
        params.update(zip(names, args))    
    
    # assign kwargs given
    if kwargs_name:
        params[kwargs_name] = {}
        for kw, value in kwargs.iteritems():
            if kw in names:
                params[kw] = value
            else:
                params[kwargs_name][kw] = value
    else:
        params.update(kwargs)
    
    # assign defaults
    if defaults:
        for pos, value in enumerate(defaults):
            if names[-len(defaults) + pos] not in params:
                params[names[-len(defaults) + pos]] = value
            
    # check we did it correctly.  Each param and only params are set
    assert set(params.iterkeys()) == (set(names)|set([args_name])|set([kwargs_name])
                                      )-set([None])
    
    return params

我看到这段代码被复制粘贴到了数十个开源项目中。应该将其封装成一个更易于调用的函数! - Erik Aronesty
1
在Python 3中,dict.iterkeys()方法已被弃用,因此params.iterkeys()只需变为paramsinspect.getargspec()在较新版本的Python 3中也已被弃用,因此请将inspect.getargspec调用替换为names,args_name,kwargs_name,defaults,_kwonlyargs,_kwonlydefaults,_annotations = inspect.getfullargspec(function) - abulka

3
这里是一种较新的方法来解决这个问题,使用inspect.signature(适用于Python 3.3+)。首先,我会给出一个可运行/测试的示例,然后展示如何使用它修改原始代码。
这是一个测试函数,它只是将给定的任何args/kwargs相加;至少需要一个参数(a),并且有一个带有默认值的关键字-only参数(b),以测试函数签名的不同方面。
def silly_sum(a, *args, b=1, **kwargs):
    return a + b + sum(args) + sum(kwargs.values())

现在让我们为silly_sum创建一个包装器,它可以像调用silly_sum一样被调用(有一个例外,我们将在下文说明),但是只将kwargs传递给包装的silly_sum函数。
def wrapper(f):
    sig = inspect.signature(f)
    def wrapped(*args, **kwargs):
        bound_args = sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        print(bound_args) # just for testing

        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args", [])) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        return f(**all_kwargs)
    return wrapped

sig.bind返回一个BoundArguments对象,但是这不会考虑默认值,除非您显式调用apply_defaults。如果没有传入*args/**kwargs,则这样做也会生成一个空元组和一个空字典。

sum_wrapped = wrapper(silly_sum)
sum_wrapped(1, c=9, d=11)
# prints <BoundArguments (a=1, args=(), b=1, kwargs={'c': 9, 'd': 11})>
# returns 22

然后,我们只需要获取参数的字典并添加任何**kwargs。不使用此包装器的例外情况是无法将*args传递给函数。这是因为没有这些参数的名称,所以我们无法将它们转换为 kwargs。如果将它们作为名为 args 的 kwarg 传递是可接受的,那么可以这样做。


以下是如何将其应用于原始代码:

import inspect


class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f
        self._f_sig = inspect.signature(f)

    def __call__(self, *args, **kwargs):
        bound_args = self._f_sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args"), []) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        hozer(**all_kwargs)
        self.f(*args, **kwargs)

将一个简单的函数(如 def _example(i, j=0): return i+j)用你的包装器进行包装,例如 example = wrapper(_example),然后调用 example(1) 会引发异常。 - abulka
嗯,调用 apply_defaults 的结果可能已经改变,以至于不会生成空元组 "args"。我稍微更新了代码来处理这个问题。 - Nathan

0

在@mikenerone提出的(最佳)解决方案的基础上,这里是原帖作者问题的解决方案:

import inspect
from functools import wraps

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        call_args = inspect.getcallargs(self.f, *args, **kwargs)
        hozer(**call_args)

        return self.f(*args, **kwargs)

def hozer(**kwargs):
    print('hozer got kwds:', kwargs)

def myadd(i, j=0):
    return i + j

o = mydec(myadd)
assert o(1,2) == 3
assert o(1) == 1
assert o(1, j=2) == 3

hozer got kwds: {'i': 1, 'j': 2}
hozer got kwds: {'i': 1, 'j': 0}
hozer got kwds: {'i': 1, 'j': 2}

这是一个通用的装饰器,它将 Python 函数的所有参数转换并合并为 `kwargs`,并仅使用这些 kwargs 调用封装的函数。
def call_via_kwargs(f):
    @wraps(f)
    def wrapper(*args, **kwds):
        call_args = inspect.getcallargs(f, *args, **kwds)
        print('kwargs:', call_args)
        return f(**call_args)
    return wrapper


@call_via_kwargs
def adder(i, j=0):
    return i + j

assert adder(1) == 1
assert adder(i=1) == 1
assert adder(1, j=2) == 3

kwargs: {'i': 1, 'j': 0}
kwargs: {'i': 1, 'j': 0}
kwargs: {'i': 1, 'j': 2}

这些解决方案正确处理默认参数。


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