我们知道,在flask框架中,我们的路由匹配就是通过有参装饰器来实现的,我们看一个简单的例子:
from flask import Flask, render_template, redirect, request, sessionapp = Flask(__name__)app.debug = Trueapp.secret_key = '123'@app.route('/index')def index(): return "这是主页"@app.route("/detail")def detail(): return "这是详情页面"if __name__ == '__main__': app.run()
这个装饰器我们见怪不怪,很明显是可以正常运行的;
Flask视图的多个装饰器
现在需求来了,我们需要给某些视图函数添加一个装饰器,用来验证用户是否是登录的状态,只有登录的用户才能访问,否则就让他跳转登录页面去登录。
很多小伙伴很机智,很快就想到了装饰器的嵌套,对的,我们可以用多个装饰器来装饰,既然这样,问题又来了,装饰器是写在route装饰器上方呢,还是下方呢?
1.源码分析
这里我们截取一点route源码
def route(self, rule, **options): def decorator(f): endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator
我们看route的源码,不难发现其实route方法也是返回了一个装饰器,然后用来装饰我们的视图函数,并且在add_url_rule中匹配了访问路径,以及处理视图函数的逻辑;
也就是说,route装饰器应该是最后来处理视图函数的逻辑的,那么很明显,route应该装饰在视图函数的外层,装饰被其他装饰器装饰过后返回的视图函数。
2.装饰器实践
有了思路后,一顿操作我把多层装饰器写出来了,代码如下
from flask import Flask, request, render_template, session, redirectapp = Flask(__name__)app.debug = Truedef wrapper(func): """验证登录状态装饰器函数""" def inner(*args,**kwargs): if session.get("username"): # 如果session中有用户信息,正常执行 ret = func(*args, **kwargs) return ret else: return redirect("/login") return inner@app.route("/login",methods=["get","post"])def login(): if request.method == "GET": return render_template("login.html") else: username = request.form.get("username") password = request.form.get("password") if username == "123" and password == "123": session["username"] = username return "登录成功!" else: return "登录失败!"@app.route("/index")@wrapperdef index(): return "这是主页"@app.route("/detail")@wrapperdef detail(): return "这是详情页面"if __name__ == '__main__': app.run()
激动的启动了我的项目,一秒都不用,啪,报错了。~~~
这是什么鬼!我们仔细看一下报错信息,视图函数映射被一个存在的末端函数inner覆盖写入了。
问题分析
报错说视图函数被一个存在的函数inner覆盖写入了,我们想一想,哪里用了inner啊,不就是装饰函数内的inner函数吗?
我们大概看一下源码,这里我截取的一部分
if endpoint is None: endpoint = _endpoint_from_view_func(view_func) options["endpoint"] = endpoint methods = options.pop("methods", None) if view_func is not None: old_func = self.view_functions.get(endpoint) if old_func is not None and old_func != view_func: raise AssertionError( "View function mapping is overwriting an " "existing endpoint function: %s" % endpoint ) self.view_functions[endpoint] = view_func
肯定有很多小伙伴看到这里,已经晕了,这里我就大概解释一下:
endpoint是我们传递的一个用来给视图函数去别名的变量,我们不传的情况下,默认是None,如果是None,那么endpoint会调用一个方法_endpoint_from_view_func去获取被装饰的函数的名字,因为我们的route装饰的是被wrapper装饰后返回的inner函数。
问题解决方法
问题的原因我们发现了,本质上就是wrapper两次装饰了不同视图函数,但是返回的都是inner函数,导致route装饰的时候出现覆盖的情况才报错。
我们只要让两次装饰后的inner函数名不一样就可以了,对不对!
方法一:endpoint
我们看过了源码,名字冲突的原因是因为每次都是去获取inner的__name__
,很明显会冲突,但是去获取名字的前提是因为我们没有传递endpoint别名,这样就好办了。
我们在使用route装饰的时候加上别名就轻松解决了。
@app.route("/index",endpoint="index")@wrapperdef index(): return "这是主页"@app.route("/detail",endpoint="detail")@wrapperdef detail(): return "这是详情页面"
修改后运行,问题迎刃而解,so easy啊!
方法二:wraps工具
同样的问题,既然是名字冲突,那么我们是不是可以想到functiontool中的wraps工具呢,就是让内层函数使用原本函数的属性字典__dict__
,也包括名字啦。
我们在写wrapper装饰器的时候使用上
from functools import wrapsdef wrapper(func): """验证登录状态装饰器函数""" @wraps(func) def inner(*args,**kwargs): if session.get("username"): # 如果session中有用户信息,正常执行 ret = func(*args, **kwargs) return ret else: return redirect("/login") return inner