0. 在 decorator 里获取原始函数的参数值
项目里做了一个通用锁,使用 decorator 来方便的包住某些需要限制并发的函数。因为并发不是函数级别的,而是根据参数来限制,所以需要把参数传到通用锁的 decorator 里,代码大致如下
def lock_decorator(key=None): def _lock_func(func): @functools.wraps(func) def wrapper(*args, **kwargs): # TODO: get lock_key lock_key = kwargs.get(key, '') with LockContext(key=lock_key): return func(*args, **kwargs) return wrapper return _lock_func @lock_decorator(key='uid') def apply_recharge(uid, amount): # ...
考虑到函数调用不一定都是带着参数名的,就是说调用时不一定所有参数都会进 **kwargs
,那就需要从 **args
里面按参数名捞参数
怎么能知道原函数的参数名列表,翻各种手册的得知可以用 inspect.getargspec(func)
来搞到,那么上面的 TODO
部分就可以改写如下
args_name = inspect.getargspec(func)[0] key_index = args_name.index(key) if len(args) > key_index: lock_key = args[key_index] else: lock_key = kwargs.get(key, '')
自此,一切都很美好
1. 在 decorator 里获取原始函数的调用参数字典
项目里又做了个通用的 Logger
,也做成 decorator 往目标函数一套,就可以打印出调用时的入参和结果,大致如下
def log_decorator(): def _log_func(func): @functools.wraps(func) def wrapper(*args, **kwargs): # TODO: get full args print('call func [{}] with args [{}] and kwargs [{}]'.format(func.__name__, args, kwargs)) ret = func(*args, **kwargs) print('func [{}] return [{}]'.format(func.__name__, ret)) return ret return wrapper return _log_func @log_decorator() def apply_recharge(uid, amount): # ...
看起来也还好,不过因为函数可能带默认参数,而且也希望看到 **args
到底传到哪个参数上,还是希望把所有参数按 Key-Value 的形式打印出来,跟处理通用锁一样,用 inspect.getargspec(func)
把参数名和默认值都摸出来,再考虑一下可变参数的情况,对上面的 TODO
部分改写如下
args_name, _, _, func_defaults = inspect.getargspec(func) parsed_kwargs = dict() # default args default_args = dict() default_start = len(args_name, func_defaults) for idx, d in enumerate(func_defaults): default_args[args_name[default_start + idx]] = d parsed_kwargs.update(default_args) # args with name varargs_start = len(args_name) for idx, a in enumerate(args[:varargs_start]): parsed_kwargs[args_name[idx]] = a # varargs if len(args) > varargs_start: parsed_kwargs['varargs'] = args[varargs_start:] # kwargs parsed_kwargs.update(kwargs) print('call func [{}] with args [{}]'.format(func.__name__, parsed_kwargs))
到这里,还是很美好
2. 多层 decorator 怎么拿到最原始函数的参数表
注意到上面两个例子里,apply_recharge
都只套了一个 decorator,如果两个一起用会发生什么?
根据 PEP318 里对 decorator 的定义
@dec2 @dec1 def func(arg1, arg2, ...): pass
等价于
def func(arg1, arg2, ...): pass func = dec2(dec1(func))
这里就出问题了,dec2
拿到的传入函数其实是 dec1
而不是 func
。不过在把 lock_decorator
和 log_decorator
混用时,不管谁写前面,func.__name__
都是原始的函数名,说明也还是有神器的地方做了穿透,但是 inspect.getargspec
又拿不到最底层函数的参数表,导致不管谁前谁后,都有问题
注意到每个 decorator 构建的时候都又封了一个 @functools.wraps(func)
,这个是干嘛的呢?以前都是无脑用,也没想过为啥要包一层这个,去掉会怎样?
去掉这个 @functools.wraps(func)
后,inspect.getargspec
还是一样的只能拿到最近一层的信息,而之前本来可以拿到底层的 func.__name__
也变成最近一层的函数名了,说明这里做了穿透。那么去看看代码吧
# functools.py from _functools import partial, reduce # update_wrapper() and wraps() are tools to help write # wrapper functions that can handle naive introspection WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__') WRAPPER_UPDATES = ('__dict__',) def update_wrapper(wrapper, wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): """Update a wrapper function to look like the wrapped function wrapper is the function to be updated wrapped is the original function assigned is a tuple naming the attributes assigned directly from the wrapped function to the wrapper function (defaults to functools.WRAPPER_ASSIGNMENTS) updated is a tuple naming the attributes of the wrapper that are updated with the corresponding attribute from the wrapped function (defaults to functools.WRAPPER_UPDATES) """ for attr in assigned: setattr(wrapper, attr, getattr(wrapped, attr)) for attr in updated: getattr(wrapper, attr).update(getattr(wrapped, attr, {})) # Return the wrapper so this can be used as a decorator via partial() return wrapper def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): """Decorator factory to apply update_wrapper() to a wrapper function Returns a decorator that invokes update_wrapper() with the decorated function as the wrapper argument and the arguments to wraps() as the remaining arguments. Default arguments are as for update_wrapper(). This is a convenience function to simplify applying partial() to update_wrapper(). """ return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
原来就是这里耍花样了,把底层函数的 ('__module__', '__name__', '__doc__')
都赋给了 decorator 封起来的这一层,欺骗更上层用 __name__
去判断时就当我是底层
那我也学这个,把 inspect.getargspec
的地方也处理下不就完了,去看看这个地方是怎么拿参数表的
# inspect.py def getargspec(func): """Get the names and default values of a function's arguments. A tuple of four things is returned: (args, varargs, varkw, defaults). 'args' is a list of the argument names (it may contain nested lists). 'varargs' and 'varkw' are the names of the * and ** arguments or None. 'defaults' is an n-tuple of the default values of the last n arguments. """ if ismethod(func): func = func.im_func if not isfunction(func): raise TypeError('{!r} is not a Python function'.format(func)) args, varargs, varkw = getargs(func.func_code) return ArgSpec(args, varargs, varkw, func.func_defaults)
看了下用到了 func.func_code
和 func.func_defaults
,按 Python 官方文档 https://docs.python.org/2/library/inspect.html 的解释,func_code
是运行时的字节码,从这里面捞参数表果然可行,那是不是我把这两个属性也传递上去就行了呢?改用自己的 wraps
如下
WRAPPER_ASSIGNMENTS = functools.WRAPPER_ASSGNMENTS + ('func_code', 'func_defaults') def my_wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): return _functool.partial(functools.update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
运行时报错,看了下错误提示,func_code
不可覆盖,这也对,都是运行时的字节码了,这个覆盖掉那包的这层 decorator 到底还有没有自己的逻辑部分
还是自己动手丰衣足食,既然 func_code
不可覆盖,我自己另外弄一个总可以了吧,而且当前需求是拿到参数表和默认参数,那就直接解出来穿透,也懒得最后再解一次。修改 my_wraps
如下
WRAPPER_ASSIGNMENTS = functools.WRAPPER_ASSIGNMENTS + ('__func_args_name__', '__func_default_args__') def my_wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = functools.WRAPPER_UPDATES): if getattr(wrapped, '__func_args_name__', None) is None: setattr(wrapped, '__func_args_name__', inspect.getargs(wrapped.func_code)[0]) func_defaults = getattr(wrapped, 'func_defaults') or () default_args = dict() default_start = len(wrapped.__func_args_name__) - len(func_defaults) for idx, d in enumerate(func_defaults): default_args[wrapped.__func_args_name__[default_start + idx]] = d setattr(wrapped, '__func_default_args__', default_args) return _functools.partial(functools.update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
同时在运行时解参数表,也用一个通用函数来实现
def parse_func(func, *args, **kwargs): parsed_kwargs = dict() # default args parsed_kwargs.update(func.__func_default_args__) # args with name varargs_start = len(func.__func_args_name__) for idx, a in enumerate(args[:varargs_start]): parsed_kwargs[func.__func_args_name__[idx]] = a # varargs if len(args) > varargs_start: parsed_kwargs['varargs'] = args[varargs_start:] # kwargs parsed_kwargs.update(kwargs) return parsed_kwargs
这样在 lock_decorator
和 log_decorator
里,用 my_wraps
来封装处理,同时在里面用 parse_func
来解析参数,就能拿到完整的参数表了
完整的测试代码见 https://gist.github.com/whusnoopy/9081544f7eaf4e9ceeaa9eba46ff28da