原文: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 context與request 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)起來