從零開始學(xué)Flask4 -- 請(qǐng)求以及應(yīng)用上下文

什么是上下文?

每一段程序都有很多外部變量。只有像Add這種簡(jiǎn)單的函數(shù)才是沒有外部變量的。一旦你的一段程序有了外部變量,這段程序就不完整,不能獨(dú)立運(yùn)行。你為了使他們運(yùn)行,就要給所有的外部變量一個(gè)一個(gè)寫一些值進(jìn)去。這些值的集合就叫上下文。

上下文作用域?

舉個(gè)例子:你有一個(gè)應(yīng)用函數(shù)返回用戶應(yīng)該跳轉(zhuǎn)到的 URL 。想象它總是會(huì)跳轉(zhuǎn)到 URL 的 next 參數(shù),或 HTTP referrer ,或索引頁(yè):

from flask import request, url_for

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

我們?cè)L問了請(qǐng)求的對(duì)象,但是當(dāng)你運(yùn)行程序時(shí)候:

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

出現(xiàn)這個(gè)錯(cuò)誤,因?yàn)槲覀儺?dāng)前并沒有可以訪問的請(qǐng)求。所以我們需要制造一個(gè)請(qǐng)求并且綁定到當(dāng)前的上下文。test_request_context方法為我們創(chuàng)建了一個(gè)RequestContext:

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

可以通過兩種方式利用這個(gè)上下文:使用 with 聲明或是調(diào)用push()和pop()方法:

>>> ctx.push()

然后我們使用請(qǐng)求對(duì)象:

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

直到我們調(diào)用pop:

>>> ctx.pop()

因?yàn)檎?qǐng)求上下文在內(nèi)部作為一個(gè)棧來(lái)維護(hù),所以你可以多次壓棧出棧。這在實(shí)現(xiàn)內(nèi)部重定向之類的東西時(shí)很方便。

上下文如何工作的?

我們看下這段代碼:

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()方法放回一個(gè)新的RequestContext對(duì)象,并結(jié)合with聲明來(lái)綁定上下文。從相同線程中被調(diào)用的一切,直到with
聲明結(jié)束前,都可以訪問全局的請(qǐng)求變量flask.request和其它)。

請(qǐng)求上下文內(nèi)部工作如同一個(gè)棧。棧頂是當(dāng)前活動(dòng)的請(qǐng)求。push把上下文添加到棧頂,pop把它移出棧。在出棧時(shí),應(yīng)用的teardown_request函數(shù)也會(huì)被執(zhí)行。

另一件需要注意的事是,請(qǐng)求上下文被壓入棧時(shí),并且沒有當(dāng)前應(yīng)用的應(yīng)用上下文,它會(huì)自動(dòng)創(chuàng)建一個(gè) 應(yīng)用上下文

請(qǐng)求上下文

當(dāng)Flask應(yīng)用真正處理請(qǐng)求時(shí),wsgi_app(environ, start_response)被調(diào)用。這個(gè)函數(shù)是按照下面的方式運(yùn)行的:

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

在Flask中處理請(qǐng)求時(shí),應(yīng)用會(huì)生成一個(gè)“請(qǐng)求上下文”對(duì)象。整個(gè)請(qǐng)求的處理過程,都會(huì)在這個(gè)上下文對(duì)象中進(jìn)行。這保證了請(qǐng)求的處理過程不被干擾。
看段代碼:

# 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()

根據(jù)_RequestContext上下文對(duì)象的定義,可以發(fā)現(xiàn),在構(gòu)造這個(gè)對(duì)象的時(shí)候添加了和Flask應(yīng)用相關(guān)的一些屬性:

app ——上下文對(duì)象的app屬性是當(dāng)前的Flask應(yīng)用;

url_adapter ——上下文對(duì)象的url_adapter屬性是通過Flask應(yīng)用中的Map實(shí)例構(gòu)造成一個(gè)MapAdapter實(shí)例,主要功能是將請(qǐng)求中的URL和Map實(shí)例中的URL規(guī)則進(jìn)行匹配;

request ——上下文對(duì)象的request屬性是通過Request類構(gòu)造的實(shí)例,反映請(qǐng)求的信息;

session ——上下文對(duì)象的session屬性存儲(chǔ)請(qǐng)求的會(huì)話信息;

g ——上下文對(duì)象的g屬性可以存儲(chǔ)全局的一些變量。

flashes ——消息閃現(xiàn)的信息。
def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
        # with語(yǔ)句中生成一個(gè)`response`對(duì)象
        ...
    return response(environ, start_response)

請(qǐng)求上下文對(duì)象包含了和請(qǐng)求處理相關(guān)的信息。同時(shí)Flask還根據(jù)werkzeug.local模塊中實(shí)現(xiàn)的一種數(shù)據(jù)結(jié)構(gòu)LocalStack用來(lái)存儲(chǔ)“請(qǐng)求上下文”對(duì)象。

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

# 創(chuàng)建一個(gè)`LocalStack`對(duì)象
>>> local_stack = LocalStack()
# 查看local_stack中存儲(chǔ)的信息
>>> local_stack._local.__storage__
{}

# 定義一個(gè)函數(shù),這個(gè)函數(shù)可以向`LocalStack`中添加數(shù)據(jù)
>>> def worker(i):
        local_stack.push(i)

# 使用3個(gè)線程運(yùn)行函數(shù)`worker`
>>> for i in range(3):
        t = threading.Thread(target=worker, args=(i,))
        t.start()

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

由上面的例子可以看出,存儲(chǔ)在LocalStack中的信息以字典的形式存在:鍵為線程/協(xié)程的標(biāo)識(shí)數(shù)值,值也是字典形式。每當(dāng)有一個(gè)線程/協(xié)程上要將一個(gè)對(duì)象push進(jìn)LocalStack棧中,會(huì)形成如上一個(gè)“鍵-值”對(duì)。這樣的一種結(jié)構(gòu)很好地實(shí)現(xiàn)了線程/協(xié)程的隔離,每個(gè)線程/協(xié)程都會(huì)根據(jù)自己線程/協(xié)程的標(biāo)識(shí)數(shù)值確定存儲(chǔ)在棧結(jié)構(gòu)中的值。

LocalStack還實(shí)現(xiàn)了push、pop、top等方法。其中top方法永遠(yuǎn)指向棧頂?shù)脑亍m數(shù)脑厥侵府?dāng)前線程/協(xié)程中最后被推入棧中的元素,即local_stack._local.stack-1
local模塊

應(yīng)用上下文

Flask 背后的設(shè)計(jì)理念之一就是,代碼在執(zhí)行時(shí)會(huì)處于兩種不同的“狀態(tài)”(states)。當(dāng)Flask對(duì)象被實(shí)例化后在模塊層次上應(yīng)用便開始隱式地處于應(yīng)用配置狀態(tài)。一直到第一個(gè)請(qǐng)求還是到達(dá)這種狀態(tài)才隱式地結(jié)束。當(dāng)應(yīng)用處于這個(gè)狀態(tài)的時(shí)候,你可以認(rèn)為下面的假設(shè)是成立的:

  • 程序員可以安全地修改應(yīng)用對(duì)象
  • 目前還沒有處理任何請(qǐng)求
  • 你必須得有一個(gè)指向應(yīng)用對(duì)象的引用來(lái)修改它。不會(huì)有某個(gè)神奇的代理變量指向你剛創(chuàng)建的或者正在修改的應(yīng)用對(duì)象的

相反,到了第二個(gè)狀態(tài),在處理請(qǐng)求時(shí),有一些其它的規(guī)則:

  • 當(dāng)一個(gè)請(qǐng)求激活時(shí),上下文的本地對(duì)象flask.request和其它對(duì)象等)指向當(dāng)前的請(qǐng)求
  • 你可以在任何時(shí)間里使用任何代碼與這些對(duì)象通信

這里有一個(gè)第三種情況,有一點(diǎn)點(diǎn)差異。有時(shí),你正在用類似請(qǐng)求處理時(shí)方式來(lái)與應(yīng)用交互,即使并沒有活動(dòng)的請(qǐng)求。想象一下你用交互式 Python shell 與應(yīng)用交互的情況,或是一個(gè)命令行應(yīng)用的情況。
current_app上下文本地變量就是應(yīng)用上下文驅(qū)動(dòng)的。

應(yīng)用上下文的作用

應(yīng)用上下問存在的主要原因是,在過去,請(qǐng)求上下文被附加了一堆函數(shù),但是又沒有什么好的解決方案。因?yàn)?Flask 設(shè)計(jì)的支柱之一是你可以在一個(gè) Python 進(jìn)程中擁有多個(gè)應(yīng)用。

那么代碼如何找到“正確的”應(yīng)用?在過去,我們推薦顯式地到處傳遞應(yīng)用,但是這會(huì)讓我們?cè)谑褂貌皇且赃@種理念設(shè)計(jì)的庫(kù)時(shí)遇到問題。

解決上述問題的常用方法是使用后面將會(huì)提到的 current_app代理對(duì)象,它被綁定到當(dāng)前請(qǐng)求的應(yīng)用的引用。既然無(wú)論如何在沒有請(qǐng)求時(shí)創(chuàng)建一個(gè)這樣的請(qǐng)求上下文是一個(gè)沒有必要的昂貴操作,應(yīng)用上下文就被引入了。

創(chuàng)建應(yīng)用上下文

有兩種方式來(lái)創(chuàng)建應(yīng)用上下文。第一種是隱式的:無(wú)論何時(shí)當(dāng)一個(gè)請(qǐng)求上下文被壓棧時(shí),如果有必要的話一個(gè)應(yīng)用上下文會(huì)被一起創(chuàng)建。由于這個(gè)原因,你可以忽略應(yīng)用上下文的存在,除非你需要它。
第二種是顯式地調(diào)用 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時(shí),應(yīng)用上下文也被用于 url_for()函數(shù)。這允許你在沒有請(qǐng)求時(shí)生成 URL 。

應(yīng)用上下文局部變量

應(yīng)用上下文會(huì)在必要時(shí)被創(chuàng)建和銷毀。它不會(huì)在線程間移動(dòng),并且也不會(huì)在不同的請(qǐng)求之間共享。正因?yàn)槿绱?,它是一個(gè)存儲(chǔ)數(shù)據(jù)庫(kù)連接信息或是別的東西的最佳位置。內(nèi)部的棧對(duì)象叫做 flask._app_ctx_stack。擴(kuò)展可以在最頂層自由地存儲(chǔ)額外信息,想象一下它們用一個(gè)充分獨(dú)特的名字在那里存儲(chǔ)信息,而不是在 flask.g對(duì)象里, flask.g 是留給用戶的代碼用的。

上下文用法

上下文的一個(gè)典型應(yīng)用場(chǎng)景就是用來(lái)緩存一些我們需要在發(fā)生請(qǐng)求之前或者要使用的資源。舉個(gè)例子,比如數(shù)據(jù)庫(kù)連接。當(dāng)我們?cè)趹?yīng)用上下文中來(lái)存儲(chǔ)東西的時(shí)候你得選擇一個(gè)唯一的名字,這是因?yàn)閼?yīng)用上下文為 Flask 應(yīng)用和擴(kuò)展所共享。

最常見的應(yīng)用就是把資源的管理分成如下兩個(gè)部分:

  • 一個(gè)緩存在上下文中的隱式資源
  • 當(dāng)上下文被銷毀時(shí)重新分配基礎(chǔ)資源

通常來(lái)講,這將會(huì)有一個(gè) get_X() 函數(shù)來(lái)創(chuàng)建資源 X ,如果它還不存在的話。 存在的話就直接返回它。另外還會(huì)有一個(gè) teardown_X() 的回調(diào)函數(shù)用于銷毀資源 X 。

如下是我們剛剛提到的連接數(shù)據(jù)庫(kù)的例子:

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()

當(dāng)get_db()這個(gè)函數(shù)第一次被調(diào)用的時(shí)候數(shù)據(jù)庫(kù)連接已經(jīng)被建立了。為了使得看起來(lái)更隱式一點(diǎn)我們可以使用 LocalProxy這個(gè)類:

from werkzeug.local import LocalProxydb = LocalProxy(get_db)

這樣的話用戶就可以直接通過訪問db來(lái)獲取數(shù)據(jù)句柄了,db已經(jīng)在內(nèi)部完成了對(duì)get_db()的調(diào)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容