首先看一段官方对functools.wraps的功能描述:
This is a convenience function for invoking update_wrapper()
as a function decorator when defining a wrapper function. It is equivalent to partial(update_wrapper, wrapped=wrapped,assigned=assigned, updated=updated)
.
Without the use of this decorator factory, the name of the example function would have been 'wrapper'
, and the docstring of the original example()
would have been lost.
如果不使用functools的wraps函数,则被包裹的函数名称和doc文本内容都会被替换为包裹者函数的相关内容了。
下面的例子就是演示了该情形。例子转自装饰器之functools模块的wraps的用途
首先我们先写一个装饰器
# 探索functools模块wraps装饰器的用途
from functools import wraps
def trace(func):
""" 装饰器 """
# @wraps(func)
def callf(*args, **kwargs):
""" A wrapper function """
print("Calling function:{}".format(func.__name__)) # Calling function:foo
res = func(*args, **kwargs)
print("Return value:{}".format(res)) # Return value:9
return res
return callf
@trace
def foo(x):
""" 返回给定数字的平方 """
return x * x
if __name__ == '__main__':
print(foo(3)) # 9
print(foo.__doc__)
help(foo)
print(foo.__name__)
# print(foo.__globals__)
t = trace(foo)
print(t)
打印结果:
Calling function:foo
Return value:9
9
A wrapper function
Help on function callf in module __main__:
callf(*args, **kwargs)
A wrapper function
callf
<function trace.<locals>.callf at 0x0000022F744D8730>
上面的装饰器例子等价于:trace(foo(3)),只是在使用装饰器时,我们不用再手动调用装饰器函数;
如果把这段代码提供给其他人调用, 他可能会想看下foo函数的帮助信息时:
1 2 3 4 5 6 |
|
这里,他可能会感到迷惑,继续敲:
1 2 |
|
最后, 他可能会看源码,找问题原因,我们知道Python中的对象都是"第一类"的,所以,trace函数会返回一个callf闭包函数,连带callf的上下文环境一并返回,所以,可以解释我们执行help(foo)的到结果了
那么,我们如果才能得到我们想要的foo的帮助信息呢,这里就要用到了functools的wraps了。
# 探索functools模块wraps装饰器的用途
from functools import wraps
def trace(func):
""" 装饰器 """
@wraps(func)
def callf(*args, **kwargs):
""" A wrapper function """
print("Calling function:{}".format(func.__name__)) # Calling function:foo
res = func(*args, **kwargs)
print("Return value:{}".format(res)) # Return value:9
return res
return callf
@trace
def foo(x):
""" 返回给定数字的平方 """
return x * x
if __name__ == '__main__':
print(foo(3)) # 9
print(foo.__doc__)
help(foo)
print(foo.__name__)
# print(foo.__globals__)
t = trace(foo)
print(t)
至于wraps的原理,通过下面部分源码,可以自行研究,在此就不予展开扩展了
# 有关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)
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
只要是知道,wraps是通过partial和update_wrapper来帮我们实现想要的结果的。
实际应用中,例如使用Flask进行开发时,对于一些敏感的页面是需要登录才能进行操作的,例如博客页面的浏览(/blog/list),博客的添加(/blog/add)和博客的删除(/blog/del/<int:id>)都应该设计为针对登录用户的操作。所以,如果一旦在地址栏中直接访问上述受限地址应该予以跳转到登录页面,先进行登录再继续后续的操作。
假设现在我们已经有了针对博客浏览,添加和删除的路由:
app.route('/blog/add', methods=['GET', 'POST'])
def blog_add():
...
return render_template('art_add.html', ...)
@app.route('/blog/del/<int:id>', methods=['GET'])
def blog_delete(id):
...
return redirect('/art/list', ...)
@app.route('/blog/list', methods=['GET'])
def blog_list():
return render_template('art_list.html', ...)
那么在这些视图函数获得执行之前,必须要做一次登录验证,这里就利用装饰器进行一次AOP操作:
#装饰器函数
def login_required(view_func):
# 5
@wraps(view_func)
def login_req_wrapper(*args,**kwargs):
if 'user' not in session: # 1
s = url_for('login',next_url=request.url) # 2
return redirect(s) # 3
else:
return view_func(*args,**kwargs) #4
return login_req_wrapper
login_required装饰器函数有如下几个点需要注意:
- 判断session中是否有user这个键。这个操作实际需要一个前提条件就是登录时要在session中添加一个user键对应当前登录的用户名称,登出时将user键值对从session中删除。
- 利用url_for构造访问路径。login为视图函数的名称,根据该视图函数可以找到路径/login,后面next_url可以为路径/login添加一个访问参数,参数名称为next_url,参数值为用户试图在未登录时访问的路径。利用该参数,可以在用户登录后直接跳转到之前要访问的页面。
- 注意,不要遗漏return关键字。
- 如果session中有user关键字意为目前有处于登录状态的用户,则直接调用路由对应的视图函数即可。并将视图函数执行结果利用return返回。
- 注意,这里必须使用@wraps。如果不使用@wraps,则每一个被login_required修饰的视图函数(view_func)都会有一样的名字login_req_wrapper,这在Flask中是不允许的!换句话说,Flask中每一个路由都需要有一个名字不同的视图函数负责响应。
现在将装饰器login_required添加在需要被验证的视图函数上即可:
app.route('/blog/add', methods=['GET', 'POST'])
@login_required
def blog_add():
...
return render_template('art_add.html', ...)
@app.route('/blog/del/<int:id>', methods=['GET'])
@login_required
def blog_delete(id):
...
return redirect('/art/list', ...)
@app.route('/blog/list', methods=['GET'])
@login_required
def blog_list():
return render_template('art_list.html', ...)
此时,如果需要浏览博客列表,在未登录的情况下访问地址:端口号/blog/list会跳转到登录界面:地址:端口号/login?next_url=...