如何将非硬编码参数传递给Python装饰器?
我的目标是创建一个简单的单元测试装饰器,它会执行一个函数,如果执行成功就什么都不做,如果失败了就打印“失败”和所有参数。我知道有内置的 unittest
包,但我这样做是为了学习装饰器。我只想实现“如果实际结果等于预期结果,就什么都不做,否则打印参数”。
我找到了一段代码,它可以打印出函数的所有参数:
def dumpArgs(func):
'''Decorator to print function call details - parameters names and effective values'''
def wrapper(*func_args, **func_kwargs):
arg_names = func.__code__.co_varnames[:func.__code__.co_argcount]
args = func_args[:len(arg_names)]
defaults = func.__defaults__ or ()
args = args + defaults[len(defaults) - (func.__code__.co_argcount - len(args)):]
params = list(zip(arg_names, args))
args = func_args[len(arg_names):]
if args: params.append(('args', args))
if func_kwargs: params.append(('kwargs', func_kwargs))
print(func.__name__ + ' (' + ', '.join('%s = %r' % p for p in params) + ' )')
return func(*func_args, **func_kwargs)
return wrapper
@dumpArgs
def test(a, b = 4, c = 'blah-blah', *args, **kwargs):
pass
test(1)
test(1, 3)
test(1, d = 5)
test(1, 2, 3, 4, 5, d = 6, g = 12.9)
输出:
test (a = 1, b = 4, c = 'blah-blah' )
test (a = 1, b = 3, c = 'blah-blah' )
test (a = 1, b = 4, c = 'blah-blah', kwargs = {'d': 5} )
test (a = 1, b = 2, c = 3, args = (4, 5), kwargs = {'g': 12.9, 'd': 6} )
我把它改成了这样,只有当函数的返回值不等于 4
时才打印参数(没有使用装饰器参数):
def get_all_func_param_name_values(func, *func_args, **func_kwargs):
arg_names = func.__code__.co_varnames[:func.__code__.co_argcount]
args = func_args[:len(arg_names)]
defaults = func.__defaults__ or ()
args = args + defaults[len(defaults) - (func.__code__.co_argcount - len(args)):]
params = list(zip(arg_names, args))
args = func_args[len(arg_names):]
if args: params.append(('args', args))
if func_kwargs: params.append(('kwargs', func_kwargs))
return '(' + ', '.join('%s = %r' % p for p in params) + ')'
def dumpArgs(func):
'''Decorator to print function call details - parameters names and effective values'''
def wrapper(*func_args, **func_kwargs):
a = func(*func_args, **func_kwargs)
if(a != 4):
return a
print("FAILURE: " + func.__name__ + get_all_func_param_name_values(func, *func_args, **func_kwargs))
return a
return wrapper
@dumpArgs
def getA(a, b = 4, c = 'blah-blah', *args, **kwargs):
return a
getA(1)
getA(1, 3)
getA(4, d = 5)
getA(1, 2, 3, 4, 5, d = 6, g = 12.9)
输出:
FAILURE: getA(a = 4, b = 4, c = 'blah-blah', kwargs = {'d': 5})
Out[21]: 1
(我不明白为什么第二行会打印出 1
。)
然后我把预期值 4
作为装饰器参数传入。正如这个回答所描述的,这要求原始的装饰器是一个嵌套函数:
def get_all_func_param_name_values(func, *func_args, **func_kwargs):
arg_names = func.__code__.co_varnames[:func.__code__.co_argcount]
args = func_args[:len(arg_names)]
defaults = func.__defaults__ or ()
args = args + defaults[len(defaults) - (func.__code__.co_argcount - len(args)):]
params = list(zip(arg_names, args))
args = func_args[len(arg_names):]
if args: params.append(('args', args))
if func_kwargs: params.append(('kwargs', func_kwargs))
return '(' + ', '.join('%s = %r' % p for p in params) + ')'
def dumpArgs(expected_value):
def dumpArgs2(func):
'''Decorator to print function call details - parameters names and effective values'''
def wrapper(*func_args, **func_kwargs):
a = func(*func_args, **func_kwargs)
if(a == expected_value):
return a
print("FAILURE: " + func.__name__ + get_all_func_param_name_values(func, *func_args, **func_kwargs))
return a
return wrapper
return dumpArgs2
@dumpArgs(4)
def getA(a, b = 4, c = 'blah-blah', *args, **kwargs):
return a
getA(1)
getA(1, 3)
getA(4, d = 5)
getA(1, 2, 3, 4, 5, d = 6, g = 12.9)
输出:
FAILURE: getA(a = 1, b = 4, c = 'blah-blah')
FAILURE: getA(a = 1, b = 3, c = 'blah-blah')
FAILURE: getA(a = 1, b = 2, c = 3, args = (4, 5), kwargs = {'g': 12.9, 'd': 6})
Out[31]: 1
(又是那个 1
...)
我不太清楚如何把这个硬编码的 4
改成一个 expected_value
参数,这个参数在每次函数调用时都能传入。我看到的所有例子(像这个)都有硬编码的参数。
我现在正在尝试
assert_expected_func_params(4, getA, 1)
assert_expected_func_params(4, getA, 1, 3)
assert_expected_func_params(4, getA, 4, d = 5)
assert_expected_func_params(4, getA, 1, 2, 3, 4, 5, d = 6, g = 12.9)
但离成功还很远。
我该如何实现一个装饰器参数,让我可以在每次函数调用时传入呢?
1 个回答
2
因为装饰器会把函数包裹起来,所以当你调用这个函数时,你可以拦截它的输入和输出。这样,你可以查找一个叫做 _expected
的关键词,把它去掉,然后调用这个函数,最后把函数返回的结果和你传入的期望值进行比较。
from functools import wraps
_empty = object() # sentinel value used to control testing
def dump_ne(func):
@wraps(func)
def decorated(*args, **kwargs):
# remove the expected value from the actual call kwargs
expected = kwargs.pop('_expected', _empty)
# call the function with rest of args and kwargs
result = func(*args, **kwargs)
# only test when _expected was passed in the kwargs
# only print when the result didn't equal expected
if expected is not _empty and expected != result:
print('FAIL: func={}, args={}, kwargs={}'.format(func.__name__, args, kwargs))
return result
return decorated
@dump_ne
def cool(thing):
return thing.upper()
print(cool('cat')) # prints 'CAT', test isn't run
for thing in ('cat', 'ice', 'cucumber'):
print(cool(thing, _expected='CUCUMBER'))
# dumps info for first 2 calls (cat, ice)