Flask的Context(上下文)學(xué)習(xí)筆記

上下文這個(gè)概念多見于文章中,是一句話中的語境,也就是語言環(huán)境。一句莫名其妙的話出現(xiàn)會(huì)讓人不理解什么意思,如果有語言環(huán)境的說明,則會(huì)更好,這就是語境對(duì)語意的影響。
上下文是一種屬性的有序序列,為駐留在環(huán)境內(nèi)的對(duì)象定義環(huán)境。在對(duì)象的激活過程中創(chuàng)建上下文,對(duì)象被配置為要求某些自動(dòng)服務(wù),如同步、事務(wù)、實(shí)時(shí)激活、安全性等等。

比如在計(jì)算機(jī)中,相對(duì)于進(jìn)程而言,上下文就是進(jìn)程執(zhí)行時(shí)的環(huán)境。具體來說就是各個(gè)變量和數(shù)據(jù),包括所有的寄存器變量、進(jìn)程打開的文件、內(nèi)存信息等??梢岳斫馍舷挛氖黔h(huán)境的一個(gè)快照,是一個(gè)用來保存狀態(tài)的對(duì)象。在程序中我們所寫的函數(shù)大都不是單獨(dú)完整的,在使用一個(gè)函數(shù)完成自身功能的時(shí)候,很可能需要同其他的部分進(jìn)行交互,需要其他外部環(huán)境變量的支持,上下文就是給外部環(huán)境的變量賦值,使函數(shù)能正確運(yùn)行。

Flask提供了兩種上下文,一種是應(yīng)用上下文(Application Context),一種是請(qǐng)求上下文(Request Context)。
可以查看Flask的文檔:應(yīng)用上下文 請(qǐng)求上下文

通俗地解釋一下application contextrequest context

  1. application 指的就是當(dāng)你調(diào)用app = Flask(name)創(chuàng)建的這個(gè)對(duì)象app;
  1. request 指的是每次http請(qǐng)求發(fā)生時(shí),WSGI server(比如gunicorn)調(diào)Flask.call()之后,在Flask對(duì)象內(nèi)部創(chuàng)建的Request對(duì)象;
  2. application 表示用于響應(yīng)WSGI請(qǐng)求的應(yīng)用本身,request 表示每次http請(qǐng)求;
  3. application的生命周期大于request,一個(gè)application存活期間,可能發(fā)生多次http請(qǐng)求,所以,也就會(huì)有多個(gè)request

請(qǐng)求上下文

from flask import request
@app.route('/')
def index():
    user_agent = request.headers.get('User-Agent')
    return '<p>Your browser is %s</p>' % user_agent```

Flask中有四種請(qǐng)求hook,分別是@before_first_request @before_request @after_request @teardown_request

如同上面的代碼一樣,在每個(gè)請(qǐng)求上下文的函數(shù)中我們都可以訪問request對(duì)象,然而request對(duì)象卻并不是全局的,因?yàn)楫?dāng)我們隨便聲明一個(gè)函數(shù)的時(shí)候,比如:

def handle_request():
    print 'handle request'
    print request.url 
if __name__=='__main__':
    handle_request()

此時(shí)運(yùn)行就會(huì)產(chǎn)生

RuntimeError: working outside of request context。

因此可知,F(xiàn)lask的request對(duì)象只有在其上下文的生命周期內(nèi)才有效,離開了請(qǐng)求的生命周期,其上下文環(huán)境不存在了,也就無法獲取request對(duì)象了。而上面所說的四種請(qǐng)求hook函數(shù),會(huì)掛載在生命周期的不同階段,因此在其內(nèi)部都可以訪問request對(duì)象。

可以使用Flask的內(nèi)部方法request_context()來構(gòu)建一個(gè)請(qǐng)求上下文

from werkzeug.test import EnvironBuilder
ctx = app.request_context(EnvironBuilder('/','http://localhost/').get_environ())
ctx.push()
try:
    print request.url
finally:
    ctx.pop()

對(duì)于Flask Web應(yīng)用來說,每個(gè)請(qǐng)求就是一個(gè)獨(dú)立的線程。請(qǐng)求之間的信息要完全隔離,避免沖突,這就需要使用到Thread Local。
Thread Local
對(duì)象是保存狀態(tài)的地方,在Python中,一個(gè)對(duì)象的狀態(tài)都被保存在對(duì)象攜帶的一個(gè)字典中,**Thread Local **則是一種特殊的對(duì)象,它的“狀態(tài)”對(duì)線程隔離 —— 也就是說每個(gè)線程對(duì)一個(gè) Thread Local 對(duì)象的修改都不會(huì)影響其他線程。這種對(duì)象的實(shí)現(xiàn)原理也非常簡單,只要以線程的 ID 來保存多份狀態(tài)字典即可,就像按照門牌號(hào)隔開的一格一格的信箱。
在Python中獲取Thread Local最簡單的方式是threading.local()

>>> import threading
>>> storage = threading.local()
>>> storage.foo = 1
>>> print(storage.foo)
1
>>> class AnotherThread(threading.Thread):
...         def run(self):
...             storage.foo = 2
...             print(storage.foo) # 這這個(gè)線程里已經(jīng)修改了
>>>
>>> another = AnotherThread()
>>> another.start()
2
>>> print(storage.foo) # 但是在主線程里并沒有修改
1

因此只要有Thread Local對(duì)象,就能讓同一個(gè)對(duì)象在多個(gè)線程下做到狀態(tài)隔離。

Flask是一個(gè)基于WerkZeug實(shí)現(xiàn)的框架,因此Flask的App Context和Request Context是基于WerkZeug的Local Stack的實(shí)現(xiàn)。這兩種上下文對(duì)象類定義在flask.ctx中,ctx.push會(huì)將當(dāng)前的上下文對(duì)象壓棧壓入flask._request_ctx_stack中,這個(gè)_request_ctx_stack同樣也是個(gè)Thread Local對(duì)象,也就是在每個(gè)線程中都不一樣,上下文壓入棧后,再次請(qǐng)求的時(shí)候都是通過_request_ctx_stack.top在棧的頂端取,所取到的永遠(yuǎn)是屬于自己線程的對(duì)象,這樣不同線程之間的上下文就做到了隔離。請(qǐng)求結(jié)束后,線程退出,ThreadLocal本地變量也隨即銷毀,然后調(diào)用ctx.pop()彈出上下文對(duì)象并回收內(nèi)存。


應(yīng)用上下文

從一個(gè) Flask App 讀入配置并啟動(dòng)開始,就進(jìn)入了 App Context,在其中我們可以訪問配置文件、打開資源文件、通過路由規(guī)則反向構(gòu)造 URL。可以看下面一段代碼:

from flask import Flask, current_app
app = Flask('__name__')

@app.route('/')
def index():
    return 'Hello, %s!' % current_app.name

current_app是一個(gè)本地代理,它的類型是werkzeug.local. LocalProxy,它所代理的即是我們的app對(duì)象,也就是說current_app == LocalProxy(app)。使用current_app是因?yàn)樗彩且粋€(gè)ThreadLocal變量,對(duì)它的改動(dòng)不會(huì)影響到其他線程??梢酝ㄟ^current_app._get_current_object()方法來獲取app對(duì)象。current_app只能在請(qǐng)求線程里存在,因此它的生命周期也是在應(yīng)用上下文里,離開了應(yīng)用上下文也就無法使用。

app = Flask('__name__')
print current_app.name

同樣會(huì)報(bào)錯(cuò):

RuntimeError: working outside of application context

和請(qǐng)求上下文一樣,同樣可以手動(dòng)創(chuàng)建應(yīng)用上下文:

with app.app_context():
    print current_app.name

這里的with語句和** with open() as f一樣,是Python提供的語法糖,可以為提供上下文環(huán)境省略簡化一部分工作。這里就簡化了其壓棧和出棧操作,請(qǐng)求線程創(chuàng)建時(shí),F(xiàn)lask會(huì)創(chuàng)建應(yīng)用上下文對(duì)象,并將其壓入flask._app_ctx_stack**的棧中,然后在線程退出前將其從棧里彈出。
應(yīng)用上下文也提供了裝飾器來修飾hook函數(shù),@teardown_request,它會(huì)在上下文生命周期結(jié)束前,也就是_app_ctc_stack出棧前被調(diào)用,可以用下面的代碼調(diào)用驗(yàn)證:

@app.teardown_appcontext
def teardown_db(exception):
    print 'teardown application'

需要注意的陷阱
當(dāng) app = Flask(name)構(gòu)造出一個(gè) Flask App 時(shí),App Context 并不會(huì)被自動(dòng)推入 Stack 中。所以此時(shí) Local Stack 的棧頂是空的,current_app也是 unbound 狀態(tài)。

>>> from flask import Flask
>>> from flask.globals import _app_ctx_stack, _request_ctx_stack
>>>
>>> app = Flask(__name__)
>>> _app_ctx_stack.top
>>> _request_ctx_stack.top
>>> _app_ctx_stack()
<LocalProxy unbound>
>>>
>>> from flask import current_app
>>> current_app
<LocalProxy unbound>

在編寫離線腳本的時(shí)候,如果直接在一個(gè) Flask-SQLAlchemy 寫成的 Model 上調(diào)用 User.query.get(user_id),就會(huì)遇到 RuntimeError。因?yàn)榇藭r(shí) App Context 還沒被推入棧中,而 Flask-SQLAlchemy 需要數(shù)據(jù)庫連接信息時(shí)就會(huì)去取 current_app.config,current_app 指向的卻是 _app_ctx_stack為空的棧頂。
解決的辦法是運(yùn)行腳本正文之前,先將 App 的 App Context 推入棧中,棧頂不為空后 current_app這個(gè) Local Proxy 對(duì)象就自然能將“取 config 屬性” 的動(dòng)作轉(zhuǎn)發(fā)到當(dāng)前 App 上。

>>> ctx = app.app_context()
>>> ctx.push()
>>> _app_ctx_stack.top
<flask.ctx.AppContext object at 0x102eac7d0>
>>> _app_ctx_stack.top is ctxTrue
>>> current_app<Flask '__main__'>
>>>
>>> ctx.pop()
>>> _app_ctx_stack.top
>>> current_app
<LocalProxy unbound>

那么為什么在應(yīng)用運(yùn)行時(shí)不需要手動(dòng) app_context().push()呢?因?yàn)?Flask App 在作為 WSGI Application 運(yùn)行時(shí),會(huì)在每個(gè)請(qǐng)求進(jìn)入的時(shí)候?qū)⒄?qǐng)求上下文推入 _request_ctx_stack中,而請(qǐng)求上下文一定是 App 上下文之中,所以推入部分的邏輯有這樣一條:如果發(fā)現(xiàn) _app_ctx_stack為空,則隱式地推入一個(gè) App 上下文。


思考部分

  • 既然在 Web 應(yīng)用運(yùn)行時(shí)里,應(yīng)用上下文 和 請(qǐng)求上下文 都是 Thread Local 的,那么為什么還要獨(dú)立二者?
  • 既然在Web應(yīng)用運(yùn)行時(shí)中,一個(gè)線程同時(shí)只處理一個(gè)請(qǐng)求,那么 _req_ctx_stack和 _app_ctx_stack肯定都是只有一個(gè)棧頂元素的。那么為什么還要用“?!边@種結(jié)構(gòu)?
  • App和Request是怎么關(guān)聯(lián)起來的?

查閱資料后發(fā)現(xiàn)第一個(gè)問題是因?yàn)樵O(shè)計(jì)初衷是為了能讓兩個(gè)以上的Flask應(yīng)用共存在一個(gè)WSGI應(yīng)用中,這樣在請(qǐng)求中,需要通過應(yīng)用上下文來獲取當(dāng)前請(qǐng)求的應(yīng)用信息。
而第二個(gè)問題則是需要考慮在非Web Runtime的環(huán)境中使用的時(shí)候,在多個(gè)App的時(shí)候,無論有多少個(gè)App,只要主動(dòng)去Push它的app context,context stack就會(huì)累積起來,這樣,棧頂永遠(yuǎn)是當(dāng)前操作的 App Context。當(dāng)一個(gè) App Context 結(jié)束的時(shí)候,相應(yīng)的棧頂元素也隨之出棧。如果在執(zhí)行過程中拋出了異常,對(duì)應(yīng)的 App Context 中注冊(cè)的 teardown函數(shù)被傳入帶有異常信息的參數(shù)。
這么一來就解釋了這兩個(gè)問題,在這種單線程運(yùn)行環(huán)境中,只有棧結(jié)構(gòu)才能保存多個(gè) Context 并在其中定位出哪個(gè)才是“當(dāng)前”。而離線腳本只需要 App 關(guān)聯(lián)的上下文,不需要構(gòu)造出請(qǐng)求,所以 App Context 也應(yīng)該和 Request Context 分離。
第三個(gè)問題

可以參考一下源碼看一下Flask是怎么實(shí)現(xiàn)的請(qǐng)求上下文:

# 代碼摘選自flask 0.5 中的ctx.py文件,
class _RequestContext(object):
    def __init__(self, app, environ):
        self.app = app 
        self.request = app.request_class(environ) 
        self.session = app.open_session(self.request) 
        self.g = _RequestGlobals()

Flask中的使用_RequestContext的方法如下:

class Flask(object): 
    def request_context(self, environ): 
        return _RequestContext(self, environ)

在Flask類中,每次請(qǐng)求都會(huì)調(diào)用這個(gè)request_context函數(shù)。這個(gè)函數(shù)則會(huì)創(chuàng)建一個(gè)_RequestContext對(duì)象,該對(duì)象需要接收WerkZeug中的environ對(duì)象作為參數(shù)。這個(gè)對(duì)象在創(chuàng)建時(shí),會(huì)把Flask實(shí)例本身作為實(shí)參傳進(jìn)去,所以雖然每次http請(qǐng)求都創(chuàng)建一個(gè)_RequestContext對(duì)象,但是每次創(chuàng)建的時(shí)候傳入的都是同一個(gè)Flask對(duì)象,因此:

由同一個(gè)Flask對(duì)象相應(yīng)請(qǐng)求創(chuàng)建的_RequestContext對(duì)象的app成員變量都共享一個(gè)application

通過Flask對(duì)象中創(chuàng)建_RequestContext對(duì)象,并將Flask自身作為參數(shù)傳入的方式實(shí)現(xiàn)了多個(gè)request context對(duì)應(yīng)一個(gè)application context。
然后可以看self.request = app.request_class(environ)這句
由于app成員變量是app = Flask(name) 這個(gè)對(duì)象,所以app.request_class就是Flask.request_class,而在Flask類的定義中:

request_class = Request
    class Request(RequestBase):
        ....

所以self.request = app.request_class(environ)實(shí)際上是創(chuàng)建了一個(gè)Request對(duì)象。由于一個(gè)http請(qǐng)求對(duì)應(yīng)一個(gè)_RequestContext對(duì)象的創(chuàng)建,而每個(gè)_RequestContext對(duì)象的創(chuàng)建對(duì)應(yīng)一個(gè)Request對(duì)象的創(chuàng)建,所以,每個(gè)http請(qǐng)求對(duì)應(yīng)一個(gè)Request對(duì)象。

因此
application 就是指app = Flask(name)對(duì)象
request 就是對(duì)應(yīng)每次http 請(qǐng)求創(chuàng)建的Request對(duì)象
Flask通過_RequestContext將App與Request關(guān)聯(lián)起來

僅為自己學(xué)習(xí)用,匯總了查閱的資料,記錄了一些筆記,轉(zhuǎn)載請(qǐng)注明資料中原作者。

參考資料
Flask 的 Context 機(jī)制
Flask進(jìn)階系列之上下文
Flask上下文的實(shí)現(xiàn)

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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