【轉(zhuǎn)載】Flask的Context(上下文)學習筆記

原文:Flask的Context(上下文)學習筆記
作者:饅頭白啊白

上下文這個概念多見于文章中让网,是一句話中的語境芝硬,也就是語言環(huán)境对碌。一句莫名其妙的話出現(xiàn)會讓人不理解什么意思,如果有語言環(huán)境的說明滥壕,則會更好纸颜,這就是語境對語意的影響。
上下文是一種屬性的有序序列绎橘,為駐留在環(huán)境內(nèi)的對象定義環(huán)境胁孙。在對象的激活過程中創(chuàng)建上下文,對象被配置為要求某些自動服務(wù)称鳞,如同步涮较、事務(wù)、實時激活冈止、安全性等等狂票。

比如在計算機中,相對于進程而言熙暴,上下文就是進程執(zhí)行時的環(huán)境闺属。具體來說就是各個變量和數(shù)據(jù),包括所有的寄存器變量怨咪、進程打開的文件屋剑、內(nèi)存信息等∈#可以理解上下文是環(huán)境的一個快照,是一個用來保存狀態(tài)的對象孕讳。在程序中我們所寫的函數(shù)大都不是單獨完整的匠楚,在使用一個函數(shù)完成自身功能的時候,很可能需要同其他的部分進行交互厂财,需要其他外部環(huán)境變量的支持芋簿,上下文就是給外部環(huán)境的變量賦值,使函數(shù)能正確運行璃饱。

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

通俗地解釋一下application contextrequest context

1.application 指的就是當你調(diào)用app = Flask(name)創(chuàng)建的這個對象app撩穿;
2.request 指的是每次http請求發(fā)生時,WSGI server(比如gunicorn)調(diào)Flask.call()之后谒撼,在Flask對象內(nèi)部創(chuàng)建的Request對象食寡;
3.application 表示用于響應(yīng)WSGI請求的應(yīng)用本身,request 表示每次http請求;
4.application的生命周期大于request廓潜,一個application存活期間抵皱,可能發(fā)生多次http請求善榛,所以,也就會有多個request

請求上下文

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中有四種請求hook(請求鉤子)呻畸,分別是@before_first_request @before_request @after_request @teardown_request

如同上面的代碼一樣移盆,在每個請求上下文的函數(shù)中我們都可以訪問request對象,然而request對象卻并不是全局的伤为,因為當我們隨便聲明一個函數(shù)的時候咒循,比如:

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

此時運行就會產(chǎn)生

RuntimeError: working outside of request context.

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

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

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

對于Flask Web應(yīng)用來說光戈,每個請求就是一個獨立的線程。請求之間的信息要完全隔離遂赠,避免沖突久妆,這就需要使用到Thread Local。
Thread Local
對象是保存狀態(tài)的地方跷睦,在Python中筷弦,一個對象的狀態(tài)都被保存在對象攜帶的一個字典中,Thread Local則是一種特殊的對象抑诸,它的“狀態(tài)”對線程隔離 —— 也就是說每個線程對一個 Thread Local 對象的修改都不會影響其他線程烂琴。這種對象的實現(xiàn)原理也非常簡單,只要以線程的 ID 來保存多份狀態(tài)字典即可蜕乡,就像按照門牌號隔開的一格一格的信箱奸绷。
在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) # 這這個線程里已經(jīng)修改了
>>>
>>> another = AnotherThread()
>>> another.start()
2
>>> print(storage.foo) # 但是在主線程里并沒有修改
1

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

Flask是一個基于WerkZeug實現(xiàn)的框架号醉,因此Flask的App Context和Request Context是基于WerkZeug的Local Stack的實現(xiàn)。這兩種上下文對象類定義在flask.ctx中辛块,ctx.push會將當前的上下文對象壓棧壓入flask._request_ctx_stack中畔派,這個_request_ctx_stack同樣也是個Thread Local對象,也就是在每個線程中都不一樣憨降,上下文壓入棧后父虑,再次請求的時候都是通過_request_ctx_stack.top在棧的頂端取,所取到的永遠是屬于自己線程的對象授药,這樣不同線程之間的上下文就做到了隔離士嚎。請求結(jié)束后呜魄,線程退出,ThreadLocal本地變量也隨即銷毀莱衩,然后調(diào)用ctx.pop()彈出上下文對象并回收內(nèi)存爵嗅。

應(yīng)用上下文

從一個 Flask App 讀入配置并啟動開始,就進入了 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是一個本地代理,它的類型是werkzeug.local. LocalProxy奋单,它所代理的即是我們的app對象锉试,也就是說current_app == LocalProxy(app)。使用current_app是因為它也是一個ThreadLocal變量览濒,對它的改動不會影響到其他線程呆盖。可以通過current_app._get_current_object()方法來獲取app對象贷笛。current_app只能在請求線程里存在应又,因此它的生命周期也是在應(yīng)用上下文里,離開了應(yīng)用上下文也就無法使用乏苦。

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

同樣會報錯:

RuntimeError: working outside of application context

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

with app.app_context():
    print current_app.name

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

@app.teardown_appcontext
def teardown_db(exception):
    print 'teardown application'
需要注意的陷阱

當 app = Flask(__name__)構(gòu)造出一個 Flask App 時鸣哀,App Context 并不會被自動推入 Stack 中架忌。所以此時 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>

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

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

那么為什么在應(yīng)用運行時不需要手動 app_context().push()呢合是?因為 Flask App 在作為 WSGI Application 運行時了罪,會在每個請求進入的時候?qū)⒄埱笊舷挛耐迫?_request_ctx_stack中,而請求上下文一定是 App 上下文之中聪全,所以推入部分的邏輯有這樣一條:如果發(fā)現(xiàn) _app_ctx_stack為空泊藕,則隱式地推入一個 App 上下文。

思考部分

● 既然在 Web 應(yīng)用運行時里荔烧,應(yīng)用上下文 和 請求上下文 都是 Thread Local 的吱七,那么為什么還要獨立二者?
● 既然在Web應(yīng)用運行時中鹤竭,一個線程同時只處理一個請求踊餐,那么 _req_ctx_stack和 _app_ctx_stack肯定都是只有一個棧頂元素的。那么為什么還要用“椡沃桑”這種結(jié)構(gòu)吝岭?
● App和Request是怎么關(guān)聯(lián)起來的?

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

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

# 代碼摘選自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類中舱馅,每次請求都會調(diào)用這個request_context函數(shù)。這個函數(shù)則會創(chuàng)建一個_RequestContext對象刀荒,該對象需要接收WerkZeug中的environ對象作為參數(shù)代嗤。這個對象在創(chuàng)建時,會把Flask實例本身作為實參傳進去缠借,所以雖然每次http請求都創(chuàng)建一個_RequestContext對象干毅,但是每次創(chuàng)建的時候傳入的都是同一個Flask對象,因此:

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

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

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

所以self.request = app.request_class(environ)實際上是創(chuàng)建了一個Request對象渠鸽。由于一個http請求對應(yīng)一個_RequestContext對象的創(chuàng)建,而每個_RequestContext對象的創(chuàng)建對應(yīng)一個Request對象的創(chuàng)建柴罐,所以徽缚,每個http請求對應(yīng)一個Request對象。

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末革屠,一起剝皮案震驚了整個濱河市凿试,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌似芝,老刑警劉巖那婉,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異党瓮,居然都是意外死亡详炬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門寞奸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痕寓,“玉大人,你說我怎么就攤上這事蝇闭。” “怎么了硬毕?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵呻引,是天一觀的道長。 經(jīng)常有香客問我吐咳,道長逻悠,這世上最難降的妖魔是什么元践? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮童谒,結(jié)果婚禮上单旁,老公的妹妹穿的比我還像新娘。我一直安慰自己饥伊,他們只是感情好象浑,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著琅豆,像睡著了一般愉豺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茫因,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天蚪拦,我揣著相機與錄音,去河邊找鬼冻押。 笑死驰贷,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的洛巢。 我是一名探鬼主播括袒,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼狼渊!你這毒婦竟也來了箱熬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤狈邑,失蹤者是張志新(化名)和其女友劉穎城须,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體米苹,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡糕伐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蘸嘶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片良瞧。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖训唱,靈堂內(nèi)的尸體忽然破棺而出褥蚯,到底是詐尸還是另有隱情,我是刑警寧澤况增,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布赞庶,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏歧强。R本人自食惡果不足惜澜薄,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摊册。 院中可真熱鬧肤京,春花似錦、人聲如沸茅特。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽温治。三九已至饭庞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間熬荆,已是汗流浹背舟山。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留卤恳,地道東北人累盗。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像突琳,于是被迫代替她去往敵國和親若债。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內(nèi)容