首页 > 技术文章 > Flask框架视图多层装饰器问题

ryxiong-blog 2019-07-11 15:59 原文

Flask中的app.route装饰器

我们知道,在flask框架中,我们的路由匹配就是通过有参装饰器来实现的,我们看一个简单的例子:

from flask import Flask, render_template, redirect, request, session
​
app = Flask(__name__)
app.debug = True
app.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, redirect
​
app = Flask(__name__)
app.debug = True
​
def 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")
@wrapper
def index():
    return "这是主页"
​
​
@app.route("/detail")
@wrapper
def 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函数。

第一次在装饰index视图的时候,endpoint被赋值为inner,添加到self.view_functions中,第二次装饰detail的时候,route又装饰了第一次装饰过detail返回的inner。注意:两个inner名字是一样的,但是内容不一样。问题就在这里了,route装饰inner的时候,发现有两个inner,名字一样,内容不一样,这个时候route就不知道改装饰哪一个了,因为第二个inner会覆盖view_functions中第一次存入的inner函数,就会出现刚才的报错。

问题解决方法

问题的原因我们发现了,本质上就是wrapper两次装饰了不同视图函数,但是返回的都是inner函数,导致route装饰的时候出现覆盖的情况才报错。

我们只要让两次装饰后的inner函数名不一样就可以了,对不对!

方法一:endpoint

我们看过了源码,名字冲突的原因是因为每次都是去获取inner的__name__,很明显会冲突,但是去获取名字的前提是因为我们没有传递endpoint别名,这样就好办了。

我们在使用route装饰的时候加上别名就轻松解决了。

@app.route("/index",endpoint="index")
@wrapper
def index():
    return "这是主页"
​
​
@app.route("/detail",endpoint="detail")
@wrapper
def detail():
    return "这是详情页面"

修改后运行,问题迎刃而解,so easy啊!

方法二:wraps工具

同样的问题,既然是名字冲突,那么我们是不是可以想到functiontool中的wraps工具呢,就是让内层函数使用原本函数的属性字典__dict__,也包括名字啦。

我们在写wrapper装饰器的时候使用上

from functools import wraps
​
def 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

修改运行,同样完美结局,excellent!

 

推荐阅读