自从夸下海口从朋友那里接下私活,就开始抽空看看有关 Flask 的东西。还记得刚入职的时候学习 Django,只是 Tutorial 那几个小教程就翻来覆去看了几遍,最终还是有些一头雾水,又觉得过于繁杂,索性就算了。这次也趁此机会把 Flask 上手一下,也越发理解了为什么有人说“自定义的 Flask 最终也会变成一个 Django”,可能后面有时间再去把 Django 拾起来就没那么困难了。

上面都是题外话,下面需要说的是一个关于 Flask 的一个小细节,首先我们需要明确一下在 Flask 中路由功能的实现过程是怎样的。

Flask 中的路由

route注册路由

在 Flask 中的路由注册是有多种方式的,在这里我们只介绍使用route装饰器的注册方式。一言以蔽之,Flask 中route的作用就是建立url与处理函数的映射。

通过阅读官方的 API 文档可以得知,route函数所必需的参数只有一个,我们只介绍一下常用参数。

  1. rule,必选参数。通过rule来指定该视图函数所对应的 url 地址。
  2. endpoint,就是url和处理函数映射关系中的一个中介。在 Flask 中一个路由的过程是这样的:rule –> endpoint –> view function。endpoint这个参数通常会被忽略,当 endpoint未指定时,Flask 会为其指定一个默认名,就是视图函数的名称。当然,你也可以显式指定endpoint
  3. methods,用于指定接受的请求方式。

综上,一般你会看到一个视图函数用以下方式进行路由注册:

1
2
3
@app.route('/', methods=['GET', 'POST'])
def index():
# whatever you want

view_functionsurl_map

这两个就是深入底层的 Flask 的路由注册最关键的两个变量。其中:

  • Flask.url_map保存所有的 (url, endpoint, method) 映射关系,是 werkzeug 中自己实现的一个Map对象。
  • Flask.view_functions保存所有的 {endpoint: function} 映射关系,本质就是一个字典对象。
    Flask 接收到请求之后,由url_map解析到对应的 ednpoint,然后通过view_functions的映射找到对应的视图函数,最终调用指定的函数,完成路由。

需要注意的细节

根据上文的分析,在url_map中,我们的urlendpoint都必须是唯一的。一个url,只能够对应一个 endpoint,必然也就只能对应一个视图函数,是一个一一对应的关系。而视图函数可以注册多个路由,是一个一对多的关系。这一点的理解对于我们下文要指出的问题非常关键。

如何在 Flask 中的视图函数上使用装饰器

对于装饰器,如果还有读者不了解的话,可以阅读一下廖雪峰的教程,其中关键的一点是对functools.wraps的使用。然而像我这样平时不太在乎这个函数签名问题的人,可能会写出下面这种代码:

1
2
3
4
5
def decorater(func):
def wrapper(*args, **kwargs):
# Do something
return func(*args, **kwargs)
return wrapper

然而,如果你在 Flask 中用于视图函数的装饰器也这么写的话,就会出现以下异常:

1
AssertionError: View function mapping is overwriting an existing endpoint function

这是为什么呢?

因为我们上文介绍过了,如果没有显式指定endpoint的时候,会默认使用视图函数的__name__属性作为endpoint的值,并且放在映射表中。原本视图函数的名称是func.__name__,而使用了上述装饰器之后,因为所返回的变成了wrapper函数,所以你的视图函数名称就变为了wrapper.__name__。当你针对多个视图函数使用了装饰器之后,在flask 的user_map中会造成endpoint的函数名冲突,进而抛出异常。

所以基本功要扎实,functools.wraps就是用来把被包装的函数名称原封不动地返回,以确保像 Flask 中这样依赖于函数名称的某些代码可以正常执行。因此,一个正确的写法应该是这样的:

1
2
3
4
5
6
7
8
import functools

def decorater(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Do something
return func(*args, **kwargs)
return wrapper

这样你的代码就可以正常运行了,当你检查url_map时会发现endpoint和视图函数一切正常。

我为什么要在 Flask 中使用装饰器?

装饰器这个特性可以极大地降低我们代码的复杂度,同时也提高了代码的复用性。因为视图函数作为“MVC”模型中的“C”,有着承上启下的作用,负责链接数据层与视图。在 Flask 中,很多有用的插件都使用装饰器的方式为视图函数增加功能,即优雅有便捷。比如说:

  • Flask-Login 中常见的@login_required,检查是否满足已登陆条件
  • Flask-Cache 中用于缓存数据等等。

例子很多,不一而足。

当然,这些优秀插件只是免除了我们一部分造轮子的麻烦,为我们提供了某些解决问题的思路。但是有些小功能你也可以自己来实现。比如说在 Flask 中,作为主要逻辑处理的视图函数并没有相应的“try-catch”机制,因此很多时候即便是开启了 debug 模式也不能捕获发生在 视图函数中的所有异常。这让我曾经的调试一度非常痛苦。后来才想起使用装饰器可以非常简单地解决这个问题,也真算是后知后觉了。大概的思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def handle_exception(func):
'''
Since the flask does not provide a function for catching and analysing
the exceptions, it's difficult that knowing what happened on the fly.
If you decorate a view with this, the wrapper can handle the exceptions
and log it automatically.

:param func: The view function to decorate.
:type func: function
'''

@wraps(func)
def decorated_view(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception, e:
current_app.logger.warn(traceback.format_exc())
return abort(500)
return decorated_view

这里只是提供了一个简单的思路:只要把这个装饰器用于视图函数,并且设置好 logger 的 handler,就可以捕获所有在视图函数执行过程中发生的异常并且记录下来。
这里的代码其实是有一点小问题的,因为在 Flask 中abort的实现是继承了HTTPException的一个异常抛出,所以上面的代码会把abort也作为异常捕获到,并且重新抛出 500,这样会在某些场景下产生困惑。比如说一个简单的 404 却被展示成服务器错误。这点小问题就留给有兴趣的读者来解决好了 :)

参考文章:

https://segmentfault.com/q/1010000003875420
http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014318435599930270c0381a3b44db991cd6d858064ac0000