从零开始学Flask4 -- 请求以及应用上下文

什么是上下文?

每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。

上下文作用域?

举个例子:你有一个应用函数返回用户应该跳转到的 URL 。想象它总是会跳转到 URL 的 next 参数,或 HTTP referrer ,或索引页:

from flask import request, url_for

def redirect_url():
    return request.args.get('next') or \
           request.referrer or \
           url_for('index')

我们访问了请求的对象,但是当你运行程序时候:

>>> redirect_url()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'request'

出现这个错误,因为我们当前并没有可以访问的请求。所以我们需要制造一个请求并且绑定到当前的上下文。test_request_context方法为我们创建了一个RequestContext:

>>> ctx = app.test_request_context('/?next=http://example.com/')

可以通过两种方式利用这个上下文:使用 with 声明或是调用push()和pop()方法:

>>> ctx.push()

然后我们使用请求对象:

>>> redirect_url()
u'http://example.com/'

直到我们调用pop:

>>> ctx.pop()

因为请求上下文在内部作为一个栈来维护,所以你可以多次压栈出栈。这在实现内部重定向之类的东西时很方便。

上下文如何工作的?

我们看下这段代码:

def wsgi_app(self, environ):
    with self.request_context(environ):
        try:
            response = self.full_dispatch_request()
        except Exception, e:
            response = self.make_response(self.handle_exception(e))
        return response(environ, start_response)

request_context()方法放回一个新的RequestContext对象,并结合with声明来绑定上下文。从相同线程中被调用的一切,直到with
声明结束前,都可以访问全局的请求变量flask.request和其它)。

请求上下文内部工作如同一个栈。栈顶是当前活动的请求。push把上下文添加到栈顶,pop把它移出栈。在出栈时,应用的teardown_request函数也会被执行。

另一件需要注意的事是,请求上下文被压入栈时,并且没有当前应用的应用上下文,它会自动创建一个 应用上下文

请求上下文

当Flask应用真正处理请求时,wsgi_app(environ, start_response)被调用。这个函数是按照下面的方式运行的:

def wsgi_app(environ, start_response):
    with self.request_context(environ):
        ...

在Flask中处理请求时,应用会生成一个“请求上下文”对象。整个请求的处理过程,都会在这个上下文对象中进行。这保证了请求的处理过程不被干扰。
看段代码:

# Flask v0.1
class _RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.
    """
    def __init__(self, app, environ):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None
    def __enter__(self):
        _request_ctx_stack.push(self)
    def __exit__(self, exc_type, exc_value, tb):
        # do not pop the request stack if we are in debug mode and an
        # exception happened.  This will allow the debugger to still
        # access the request object in the interactive shell.
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

根据_RequestContext上下文对象的定义,可以发现,在构造这个对象的时候添加了和Flask应用相关的一些属性:

app ——上下文对象的app属性是当前的Flask应用;

url_adapter ——上下文对象的url_adapter属性是通过Flask应用中的Map实例构造成一个MapAdapter实例,主要功能是将请求中的URL和Map实例中的URL规则进行匹配;

request ——上下文对象的request属性是通过Request类构造的实例,反映请求的信息;

session ——上下文对象的session属性存储请求的会话信息;

g ——上下文对象的g属性可以存储全局的一些变量。

flashes ——消息闪现的信息。
def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
        # with语句中生成一个`response`对象
        ...
    return response(environ, start_response)

请求上下文对象包含了和请求处理相关的信息。同时Flask还根据werkzeug.local模块中实现的一种数据结构LocalStack用来存储“请求上下文”对象。

  • LocalStack详解
>>> from werkzeug.local import LocalStack
>>> import threading

# 创建一个`LocalStack`对象
>>> local_stack = LocalStack()
# 查看local_stack中存储的信息
>>> local_stack._local.__storage__
{}

# 定义一个函数,这个函数可以向`LocalStack`中添加数据
>>> def worker(i):
        local_stack.push(i)

# 使用3个线程运行函数`worker`
>>> for i in range(3):
        t = threading.Thread(target=worker, args=(i,))
        t.start()

# 再次查看local_stack中存储的信息
>>> local_stack._local.__storage__
{<greenlet.greenlet at 0x4bee5a0>: {'stack': [2]},
 <greenlet.greenlet at 0x4bee638>: {'stack': [1]},
 <greenlet.greenlet at 0x4bee6d0>: {'stack': [0]}
}

由上面的例子可以看出,存储在LocalStack中的信息以字典的形式存在:键为线程/协程的标识数值,值也是字典形式。每当有一个线程/协程上要将一个对象push进LocalStack栈中,会形成如上一个“键-值”对。这样的一种结构很好地实现了线程/协程的隔离,每个线程/协程都会根据自己线程/协程的标识数值确定存储在栈结构中的值。

LocalStack还实现了push、pop、top等方法。其中top方法永远指向栈顶的元素。栈顶的元素是指当前线程/协程中最后被推入栈中的元素,即local_stack._local.stack-1
local模块

应用上下文

Flask 背后的设计理念之一就是,代码在执行时会处于两种不同的“状态”(states)。当Flask对象被实例化后在模块层次上应用便开始隐式地处于应用配置状态。一直到第一个请求还是到达这种状态才隐式地结束。当应用处于这个状态的时候,你可以认为下面的假设是成立的:

  • 程序员可以安全地修改应用对象
  • 目前还没有处理任何请求
  • 你必须得有一个指向应用对象的引用来修改它。不会有某个神奇的代理变量指向你刚创建的或者正在修改的应用对象的

相反,到了第二个状态,在处理请求时,有一些其它的规则:

  • 当一个请求激活时,上下文的本地对象flask.request和其它对象等)指向当前的请求
  • 你可以在任何时间里使用任何代码与这些对象通信

这里有一个第三种情况,有一点点差异。有时,你正在用类似请求处理时方式来与应用交互,即使并没有活动的请求。想象一下你用交互式 Python shell 与应用交互的情况,或是一个命令行应用的情况。
current_app上下文本地变量就是应用上下文驱动的。

应用上下文的作用

应用上下问存在的主要原因是,在过去,请求上下文被附加了一堆函数,但是又没有什么好的解决方案。因为 Flask 设计的支柱之一是你可以在一个 Python 进程中拥有多个应用。

那么代码如何找到“正确的”应用?在过去,我们推荐显式地到处传递应用,但是这会让我们在使用不是以这种理念设计的库时遇到问题。

解决上述问题的常用方法是使用后面将会提到的 current_app代理对象,它被绑定到当前请求的应用的引用。既然无论如何在没有请求时创建一个这样的请求上下文是一个没有必要的昂贵操作,应用上下文就被引入了。

创建应用上下文

有两种方式来创建应用上下文。第一种是隐式的:无论何时当一个请求上下文被压栈时,如果有必要的话一个应用上下文会被一起创建。由于这个原因,你可以忽略应用上下文的存在,除非你需要它。
第二种是显式地调用 app_context()方法:

from flask import Flask, current_app

app = Flask(__name__)
with app.app_context():
    # within this block, current_app points to app.
    print current_app.name

在配置了SERVER_NAME时,应用上下文也被用于 url_for()函数。这允许你在没有请求时生成 URL 。

应用上下文局部变量

应用上下文会在必要时被创建和销毁。它不会在线程间移动,并且也不会在不同的请求之间共享。正因为如此,它是一个存储数据库连接信息或是别的东西的最佳位置。内部的栈对象叫做 flask._app_ctx_stack。扩展可以在最顶层自由地存储额外信息,想象一下它们用一个充分独特的名字在那里存储信息,而不是在 flask.g对象里, flask.g 是留给用户的代码用的。

上下文用法

上下文的一个典型应用场景就是用来缓存一些我们需要在发生请求之前或者要使用的资源。举个例子,比如数据库连接。当我们在应用上下文中来存储东西的时候你得选择一个唯一的名字,这是因为应用上下文为 Flask 应用和扩展所共享。

最常见的应用就是把资源的管理分成如下两个部分:

  • 一个缓存在上下文中的隐式资源
  • 当上下文被销毁时重新分配基础资源

通常来讲,这将会有一个 get_X() 函数来创建资源 X ,如果它还不存在的话。 存在的话就直接返回它。另外还会有一个 teardown_X() 的回调函数用于销毁资源 X 。

如下是我们刚刚提到的连接数据库的例子:

import sqlite3
from flask import g

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = connect_to_database()
    return db

@app.teardown_appcontext
def teardown_db(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

当get_db()这个函数第一次被调用的时候数据库连接已经被建立了。为了使得看起来更隐式一点我们可以使用 LocalProxy这个类:

from werkzeug.local import LocalProxydb = LocalProxy(get_db)

这样的话用户就可以直接通过访问db来获取数据句柄了,db已经在内部完成了对get_db()的调用。

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

推荐阅读更多精彩内容