视图装饰器
Python 装饰器是用于转换其它函数的函数。当一个装饰的函数被调用的时候,装饰器也会被调用。接着装饰器就会采取行动,修改参数,停止执行或者调用原始函数。我们可以使用装饰器来包装视图,让它们在执行之前运行我们希望的代码。
@decorator_function
def decorated():
pass
如果你已经浏览了 Flask 教程,在这个代码块的语法看起来很熟悉。@app.route 是用于为 Flask 应用程序的视图函数匹配 URLs 的装饰器。
让我们看看其它的装饰器,你可能会在你的 Flask 应用中使用到它们。
认证
Flask-Login 扩展可以很容易地实现登录系统。除了处理用户认证的细节,Flask-Login 提供给我们一个装饰器,它用来限制只允许登录的用户访问某些视图: @login_required
。
# app.py
from flask import render_template
from flask.ext.login import login_required, current_user
@app.route('/')
def index():
return render_template("index.html")
@app.route('/dashboard')
@login_required
def account():
return render_template("account.html")
<strong>
@app.route
应该是外层的视图装饰器(换句话说, @app.route
应该在所有装饰器的最前面)。
</strong>
一个登录过的用户才能够访问 /dashboard 路由。我们可以配置 Flask-Login,让未登录的用户重定向到一个登录页,返回一个 HTTP 401 状态码或者我们想要它做的任何东西。
缓存
想象下在 CNN 以及其它一些新闻网站中提到我们的应用程序,我们可能会在不久之后接收每秒数千次的请求。我们的主页针对每一次请求都要多次访问数据库,因此所有这些因素都会导致系统越来越慢,用户访问等待的时间越来越长。如何才能加快访问速度,让所有的访客都不会错过我们的网站?
有很多好的答案,但是这一章是关于缓存,因此我们就来讨论它。确切地来说,我们将要使用 Flask-Cache 扩展。这个扩展提供我们一个装饰器,我们可以在我们的首页视图上使用这个装饰器用来在一段时间内缓存响应。
Flask-Cache 可以被配置成与一堆不同的缓存后端一起工作。一个流行的选择是 Redis,Redis 很容易设置和使用。假设 Flask-Cache 已经配置好,这个代码块显示我们的缓存装饰器视图是什么样子的。
# app.py
from flask.ext.cache import Cache
from flask import Flask
app = Flask()
# We'd normally include configuration settings in this call
cache = Cache(app)
@app.route('/')
@cache.cached(timeout=60)
def index():
[...] # Make a few database calls to get the information we need
return render_template(
'index.html',
latest_posts=latest_posts,
recent_users=recent_users,
recent_photos=recent_photos
)
现在函数将每 60 秒会执行一次,因为 60 秒后缓存就过期。响应将会保存在我们的缓存中,在缓存没有过期之前,所有针对首页的请求都会直接从缓存中读取。
Flask-Cache 也为我们提供了 memoize 函数 — 或者缓存一个函数调用某些参数的结果。你甚至可以缓存计算开销很高的 Jinja2 模板片段。
自定义装饰器
对于这篇文章,先让我们想象下我们有一个应用程序,该应用程序每个月都会向用户收费。如果用户的账号已经过期,我们将会重定向到收费页面并且让用户升级。
# myapp/util.py
from functools import wraps
from datetime import datetime
from flask import flash, redirect, url_for
from flask.ext.login import current_user
def check_expired(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if datetime.utcnow() > current_user.account_expires:
flash("Your account has expired. Update your billing info.")
return redirect(url_for('account_billing'))
return func(*args, **kwargs)
return decorated_function
- 当一个函数使用
@check_expired
装饰,check_expired()
被调用并且被装饰的 函数被作为参数进行传递。 -
@wraps
是一个装饰器,它做了一些工作使得decorated_function()
看起来像func()
。这使得函数的行为多了几分自然。 -
decorated_function
将会获取所有我们传递给原始视图函数func()
的args
和kwargs
。我们在这里检查用户的账号是否过期。如果已经过期的话, 我们将会闪现一条消息并且重定向到一个收费页面。 - 现在我们已经做了我们想要做的事情,我们使用它原始的参数运行被装饰的视图函数
func()
。
当我们叠加装饰器的时候,最上层的装饰器会首先运行,接着调用下一行的下一个函数:要么是视图函数,要么就是装饰器。装饰器的语法只是 Python 提供的一个语法糖。
# This code:
@foo
@bar
def one():
pass
r1 = one()
# is the same as this code:
def two():
pass
two = foo(bar(two))
r2 = two()
r1 == r2 # True
此代码块展示了一个使用我们自定义的装饰器和来自 Flask-Login 扩展的 @login_required 装饰器的示例。我们可以通过叠加使用多个装饰器。
# myapp/views.py
from flask import render_template
from flask.ext.login import login_required
from . import app
from .util import check_expired
@app.route('/use_app')
@login_required
@check_expired
def use_app():
"""Use our amazing app."""
# [...]
return render_template('use_app.html')
@app.route('/account/billing')
@login_required
def account_billing():
"""Update your billing info."""
# [...]
return render_template('account/billing.html')
现在当一个用户试图访问 /use_app,check_expired() 将会确保在运行视图函数之前用户的账号没有过期。
URL 转换器(converters)
内置转换器(converters)
当你在 Flask 中定义路由的时候,你可以指定路由的一部分,它们将会转换成 Python 变量并且传递到视图函数。
@app.route('/user/<username>')
def profile(username):
pass
在 URL 中的 <username> 将会作为 username 参数传入到视图。你也可以指定一个转换器,用来在变量传入视图之前对其进行过滤筛选。
@app.route('/user/id/<int:user_id>')
def profile(user_id):
pass
在这个代码块中,URL:http://myapp.com/user/id/Q29kZUxlc3NvbiEh 将会返回一个 404 状态码 – 未找到。这是因为 URL 中的 user_id 要求的是一个整数但实际上是一个字符串。
我们也可以有第二个视图用来处理 user_id 为字符串,/user/id/Q29kZUxlc3NvbiEh/ 可以调用该视图而 /user/id/124 可以调用第一个视图。
下面描述了 Flask 内置的 URL 转换器。
- string: 不带斜杠(默认值)的任何文本。
- int: 整数。
- float: 像 int,但是只允许浮点值。
- path:像字符串,但是包含斜杠。
自定义转换器(converters)
我们也能准备自定义转换器来满足自己的需求。在 Reddit 上 — 一个受欢迎的链接共享网站 — 用户创建和主持的以主题为基础的讨论和链接共享的社区。例如,/r/python 和 /r/flask 就是分别用 URL:redit.com/r/python 和 reddit.com/r/flask 来表示。Reddit 一个有意思的功能就是你可以查看多个 subreddits 的文章,通过在 URL 中使用加号(+)来连接每一个 subreddits 的名称,例如,reddit.com/r/python+flask。
我们可以在我们自己的 Flask 应用程序中使用一个自定义的转换器来实现这个功能。我们将接受通过加号(+)分离的任意数量的元素,转换它们成一个列表(这里实现了一个叫做 ListConverter 的类)并且把列表元素传给视图函数。
# myapp/util.py
from werkzeug.routing import BaseConverter
class ListConverter(BaseConverter):
def to_python(self, value):
return value.split('+')
def to_url(self, values):
return '+'.join(BaseConverter.to_url(value)
for value in values)
我们需要定义两个方法:to_python()
和 to_url()
。正如名称暗示的一样,to_python()
是用于转换 URL 中的路径成为一个 Python 对象,该对象将会传递给视图;to_url()
是被 url_for()
用来把参数转换为合适的形式的 URL。
为了使用我们的 ListConverter,我们首先必须告诉 Flask 它的存在。
# /myapp/__init__.py
from flask import Flask
app = Flask(__name__)
from .util import ListConverter
app.url_map.converters['list'] = ListConverter
这里可能有机会碰到循环导入的问题如果你的 util 模块有 from . import app 这一行。这是我为什么要等到 app 已经初始化后才导入 ListConverter。
现在我们就可以像使用内置的转换器一样使用自己的转换器。我们可以在 @app.route() 中使用 “list”,就像使用内置的 int,float,string,path 一样。
# myapp/views.py
from . import app
@app.route('/r/\\<list:subreddits\\>')
def subreddit_home(subreddits):
"""Show all of the posts for the given subreddits."""
posts = []
for subreddit in subreddits:
posts.extend(subreddit.posts)
return render_template('/r/index.html', posts=posts)
这应该会像 Reddit 的多 reddit 系统一样工作。同样的方法可以被使用来做我们想要的任何 URL 转换。
摘要
- Flask-Login 中的 @login_required 装饰器帮助你限制只允许登录的用户访问视图。
- Flask-Cache 扩展为你提供了大量的装饰器用来实现各种的缓存方法。
- 我们能够开发自定义视图装饰器用来帮助我们组织代码并且坚持 DRY(不要重复你自己)的编码原则。
- 自定义的 URL 转换器是实现涉及到 URL 的创新功能的一个很好的方式。