我正在编写一个装饰器,需要在调用它正在装饰的函数之前调用其他函数。修饰函数可能有位置参数,但装饰器将调用的函数只能接受关键字参数。有没有人有方便的方法将位置参数转换为关键字参数?
我知道我可以获得装饰函数的变量名列表:
>>> 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添加任何不在那里的东西,并希望最好的吗?
答案 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}
这些解决方案正确处理默认参数。