给一个函数添加装饰器
给函数添加一个包装,以添加额外功能(比如计时、记录日志等等)
def outer(func):
def inner(*args, **kwargs):
print('{} is called'.format(func.__name__) )
res = func(*args, **kwargs)
print(res)
return res
return inner
@outer
def add(a, b):
“”“this a add function”“”
return a + b
add(1,2)
结果
add is called
3
如何保存被装饰函数的元数据(附底层原理,需要一定的功底)
上面我们定义了一个装饰器,并将它作用在add函数上,但是有一个问题,被装饰过add函数一些重要元数据被丢失了,比如函数名、函数文档等等
print(add.__name__)
print(add.__doc__)
结果
inner
None
这是因为被装饰过之后,add这个变量名已经指向装饰器的内部的inner函数对象了,所以才有上面的结果
如果想保存被装饰函数的元数据,需要为装饰器的内部函数也添加一个装饰器,它是位于python标准库的functools
模块中的wraps
函数
from functools import wraps
def outer(func):
@wraps(func)
def inner(*args, **kwargs):
print('{} is called'.format(func.__name__))
res = func(*args, **kwargs)
print(res)
return res
return inner
@outer
def add(a, b):
"""this is a add function"""
return a + b
add(1, 2)
print('==============')
print(add.__name__)
print(add.__doc__)
结果
add is called
3
==============
add
this is a add function
加上这个@wraps装饰器,add函数的元数据就不会丢失了,@wraps装饰器的一个重要特性就是它可以让被装饰过的函数通过__wrapped__属性来访问被包装的自己,绕过装饰器,这个原理下面讲过wraps
源码就会明白了,听着有点绕,那就先让我们直接看代码吧
print(add.__wrapped__(1, 2))
结果
3
你看根本没进入到我们定义的装饰器的inner里面去,是不是很神奇,接下来我们去看下functools.wraps
源码,一探究竟,在探究wraps
源码之前,我先说下,要想理解这个源码,需要知道functools.partial
这个类的知识,
wraps
源码以及涉及到的源码如下
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
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:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# 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)
高能预警,需要一定的python功底
@wraps装饰器的等价语法: inner = wraps(func)(inner)
wrap函数的文档的意思我翻译一下:这是一个装饰器工厂函数,它将update_wrapper()
这个函数绑定到包装函数上,wraps
函数返回一个partial
对象,这是一个偏函数对象,跟高等数学里偏导数概念有点类似,当我们调用add的时候,其实就是调用这个对象,partial
实现了__call__方法,最终其实就是调用这个类的第一个参数update_wrapper
函数,并将被装饰的函数传入以及固定的三个其他参数,在update_wrapper
里完成了元数据的复制,注意还将被包装函数赋值给包装函数的__wrapped__
属性,并返回包装函数,此时的包装函数inner已经具有add的元数据(__module__, __name__,__qualname____doc__, __annotations__),
partial`源码如下
class partial:
"""New function with partial application of the given arguments
and keywords.
"""
__slots__ = "func", "args", "keywords", "__dict__", "__weakref__"
def __new__(*args, **keywords):
if not args:
raise TypeError("descriptor '__new__' of partial needs an argument")
if len(args) < 2:
raise TypeError("type 'partial' takes at least one argument")
cls, func, *args = args
if not callable(func):
raise TypeError("the first argument must be callable")
args = tuple(args)
if hasattr(func, "func"):
args = func.args + args
tmpkw = func.keywords.copy()
tmpkw.update(keywords)
keywords = tmpkw
del tmpkw
func = func.func
self = super(partial, cls).__new__(cls)
self.func = func
self.args = args
self.keywords = keywords
return self
def __call__(*args, **keywords):
if not args:
raise TypeError("descriptor '__call__' of partial needs an argument")
self, *args = args
newkeywords = self.keywords.copy()
newkeywords.update(keywords)
return self.func(*self.args, *args, **newkeywords)
@recursive_repr()
def __repr__(self):
qualname = type(self).__qualname__
args = [repr(self.func)]
args.extend(repr(x) for x in self.args)
args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
if type(self).__module__ == "functools":
return f"functools.{qualname}({', '.join(args)})"
return f"{qualname}({', '.join(args)})"
def __reduce__(self):
return type(self), (self.func,), (self.func, self.args,
self.keywords or None, self.__dict__ or None)
def __setstate__(self, state):
if not isinstance(state, tuple):
raise TypeError("argument to __setstate__ must be a tuple")
if len(state) != 4:
raise TypeError(f"expected 4 items in state, got {len(state)}")
func, args, kwds, namespace = state
if (not callable(func) or not isinstance(args, tuple) or
(kwds is not None and not isinstance(kwds, dict)) or
(namespace is not None and not isinstance(namespace, dict))):
raise TypeError("invalid partial state")
args = tuple(args) # just in case it's a subclass
if kwds is None:
kwds = {}
elif type(kwds) is not dict: # XXX does it need to be *exactly* dict?
kwds = dict(kwds)
if namespace is None:
namespace = {}
self.__dict__ = namespace
self.func = func
self.args = args
self.keywords = kwds
核心在于__call__
方法,好好理解下
如何对装饰器解包
如果装饰器的内部函数使用了@wraps
装饰,那么就可以通过访问__wrapped__属性来获取对原始函数的访问,从而绕过装饰器,具体原理在上面一节已经说了。