從零開始學Flask4 -- 請求以及應用上下文

什么是上下文?

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

上下文作用域?

舉個例子:你有一個應用函數(shù)返回用戶應該跳轉(zhuǎn)到的 URL 。想象它總是會跳轉(zhuǎn)到 URL 的 next 參數(shù),或 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'

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

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

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

>>> ctx.push()

然后我們使用請求對象:

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

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

>>> ctx.pop()

因為請求上下文在內(nèi)部作為一個棧來維護,所以你可以多次壓棧出棧。這在實現(xiàn)內(nèi)部重定向之類的東西時很方便。

上下文如何工作的?

我們看下這段代碼:

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對象,并結(jié)合with聲明來綁定上下文。從相同線程中被調(diào)用的一切,直到with
聲明結(jié)束前,都可以訪問全局的請求變量flask.request和其它)。

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

另一件需要注意的事是,請求上下文被壓入棧時,并且沒有當前應用的應用上下文,它會自動創(chuàng)建一個 應用上下文 。

請求上下文

當Flask應用真正處理請求時,wsgi_app(environ, start_response)被調(diào)用。這個函數(shù)是按照下面的方式運行的:

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

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

app ——上下文對象的app屬性是當前的Flask應用;

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

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

session ——上下文對象的session屬性存儲請求的會話信息;

g ——上下文對象的g屬性可以存儲全局的一些變量。

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

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

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

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

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

# 使用3個線程運行函數(shù)`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中的信息以字典的形式存在:鍵為線程/協(xié)程的標識數(shù)值,值也是字典形式。每當有一個線程/協(xié)程上要將一個對象push進LocalStack棧中,會形成如上一個“鍵-值”對。這樣的一種結(jié)構(gòu)很好地實現(xiàn)了線程/協(xié)程的隔離,每個線程/協(xié)程都會根據(jù)自己線程/協(xié)程的標識數(shù)值確定存儲在棧結(jié)構(gòu)中的值。

LocalStack還實現(xiàn)了push、pop、top等方法。其中top方法永遠指向棧頂?shù)脑?。棧頂?shù)脑厥侵府斍熬€程/協(xié)程中最后被推入棧中的元素,即local_stack._local.stack-1
local模塊

應用上下文

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

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

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

  • 當一個請求激活時,上下文的本地對象flask.request和其它對象等)指向當前的請求
  • 你可以在任何時間里使用任何代碼與這些對象通信

這里有一個第三種情況,有一點點差異。有時,你正在用類似請求處理時方式來與應用交互,即使并沒有活動的請求。想象一下你用交互式 Python shell 與應用交互的情況,或是一個命令行應用的情況。
current_app上下文本地變量就是應用上下文驅(qū)動的。

應用上下文的作用

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

那么代碼如何找到“正確的”應用?在過去,我們推薦顯式地到處傳遞應用,但是這會讓我們在使用不是以這種理念設(shè)計的庫時遇到問題。

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

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

有兩種方式來創(chuàng)建應用上下文。第一種是隱式的:無論何時當一個請求上下文被壓棧時,如果有必要的話一個應用上下文會被一起創(chuà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時,應用上下文也被用于 url_for()函數(shù)。這允許你在沒有請求時生成 URL 。

應用上下文局部變量

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

上下文用法

上下文的一個典型應用場景就是用來緩存一些我們需要在發(fā)生請求之前或者要使用的資源。舉個例子,比如數(shù)據(jù)庫連接。當我們在應用上下文中來存儲東西的時候你得選擇一個唯一的名字,這是因為應用上下文為 Flask 應用和擴展所共享。

最常見的應用就是把資源的管理分成如下兩個部分:

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

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

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

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()這個函數(shù)第一次被調(diào)用的時候數(shù)據(jù)庫連接已經(jīng)被建立了。為了使得看起來更隱式一點我們可以使用 LocalProxy這個類:

from werkzeug.local import LocalProxydb = LocalProxy(get_db)

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

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

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

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