Python将args转换为kwargs

时间:2009-05-06 18:14:35

标签: python decorator

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

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

>>> 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添加任何不在那里的东西,并希望最好的吗?

7 个答案:

答案 0 :(得分:17)

任何以位置方式传递的arg都将传递给* args。任何作为关键字传递的arg都将传递给** kwargs。 如果你有位置参数值和名称,那么你可以这样做:

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

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

答案 1 :(得分:11)

如果您使用的是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,那么该密钥将不会出现在dict中,并且您已完成(我假设没有*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 function,了解与inspect.getcallargs()类似的功能(但更强大)。

答案 2 :(得分:5)

注意 - co_varnames将包含局部变量和关键字。这可能无关紧要,因为zip会截断较短的序列,但如果传递错误的args数量,可能会导致混淆错误消息。

您可以使用func_code.co_varnames[:func_code.co_argcount]来避免这种情况,但更好的方法是使用inspect模块。即:

import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

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

答案 3 :(得分:5)

嗯,这可能是矫枉过正的。我为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

答案 4 :(得分:0)

这是使用inspect.signature解决此问题的新方法(适用于Python 3.3+)。我将给出一个示例,该示例可以首先自己运行/测试,然后展示如何使用它来修改原始代码。

这是一个测试函数,它仅对赋予它的所有args / kwarg进行求和;至少需要一个参数(a),并且只有一个带有默认值(b)的仅关键字的参数,用于测试函数签名的不同方面。

def silly_sum(a, *args, b=1, **kwargs):
    return a + b + sum(args) + sum(kwargs.values())

现在让我们为silly_sum做一个包装器,可以用与silly_sum相同的方式调用(我们将要遇到的例外情况),但是只能将kwargs传递给包装的{{ 1}}。

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,这样做也会为args生成一个空元组,为kwargs生成一个空dict。

**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无法传递给函数。这是因为没有名称,所以我们不能将它们转换为kwargs。如果可以接受通过名为args的kwarg传递它们,则可以这样做。


这是将其应用于原始代码的方法:

*args

答案 5 :(得分:0)

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'错误

答案 6 :(得分: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}

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