Flask 视图和路由的进阶技能

视图装饰器

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()argskwargs。我们在这里检查用户的账号是否过期。如果已经过期的话, 我们将会闪现一条消息并且重定向到一个收费页面。
  • 现在我们已经做了我们想要做的事情,我们使用它原始的参数运行被装饰的视图函数 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 的创新功能的一个很好的方式。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,569评论 18 139
  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,158评论 22 257
  • 第二部分 Blog例子 第八章 用户验证 大部分程序需要追踪用户身份。当用户连接到程序,通过一系列步骤使自己的身份...
    易木成华阅读 1,276评论 0 4
  • 最近在学习flask,用到flask-login,发现网上只有0.1版本的中文文档,看了官方已经0.4了,并且添加...
    ZZES_ZCDC阅读 5,928评论 3 24
  • 一条腿已残 是爱情 一条腿苟存 是生活 命运之刀 截断双腿 他决心为单爱 一刀两断 她却以爱的假肢 苦苦支着 苦苦...
    ecf33a82606f阅读 316评论 0 0