flask中的上下文分兩種,application context和request context,即應用上下文和請求上下文。
從名字上看,可能會有誤解,認為應用上下文是一個應用的全局變量,所有請求都可以通路修改其中的内容;而請求上下文則是請求内可通路的内容。 但事實上,這兩者并不是全局與局部的關系,它們都處于一個請求的局部中。
先說結論 : 每個請求的g都是獨立的,并且在整個請求内都是可通路修改的。
下面來研究一下。
上下文類的定義:
上下文類定義在flask.ctx子產品中
class AppContext(object): def __init__( self , app):
self.app = app
self.url_adapter = app.create_url_adapter( None)
self.g = app.app_ctx_globals_class()
# Like request context, app contexts can be pushed multiple times
# but there a basic "refcount" is enough to track them.
self._refcnt = 0
檢視了源代碼,AppContext類即是應用上下文,可以看到裡面隻儲存了幾個變量,其中比較重要的有: app是目前web應用對象的引用,如Flask;還有g,用來儲存需要在每個請求中需要用到的請求内全局變量。
class RequestContext(object): def __init__( self , app , environ , request= None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = app.create_url_adapter( self.request)
self.flashes = None
self.session = None
# Request contexts can be pushed multiple times and interleaved with
# other request contexts. Now only if the last level is popped we
# get rid of them. Additionally if an application context is missing
# one is created implicitly so for each level we add this information
self._implicit_app_ctx_stack = []
RequestContext即請求上下文,其中有我們熟悉的request和session,app和應用上下文中的app含義相同。
上下文對象的作用域
那麼這兩種上下文運作時是怎麼被使用的呢?
線程有個叫做ThreadLocal的類,也就是通常實作線程隔離的類。而werkzeug自己實作了它的線程隔離類:werkzeug.local.Local。LocalStack就是用Local實作的。
在flask.globals子產品中定義了兩個LocalStack對象: _request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
LocalStack是flask定義的線程隔離的棧存儲對象,分别用來儲存應用和請求上下文。 它是線程隔離的意思就是說,對于不同的線程,它們通路這兩個對象看到的結果是不一樣的、完全隔離的。這是根據pid的不同實作的,類似于門牌号。
而每個傳給flask對象的請求,都是在不同的線程中處理,而且同一時刻每個線程隻處理一個請求。是以對于每個請求來說,它們完全不用擔心自己上下文中的資料被别的請求所修改。
然後就可以解釋這個特性:從flask子產品中引入的g、session、request、current_app是怎麼做到同一個對象能在所有請求中使用并且不會沖突。
這幾個對象還是定義在flask.globals中: current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object , 'request'))
session = LocalProxy(partial(_lookup_req_object , 'session'))
g = LocalProxy(partial(_lookup_app_object , 'g'))
LocalProxy類的構造函數接收一個callable參數,上面這幾個就傳入了一個偏函數。以g為例,當對g進行操作時,就會調用作為參數的偏函數,并把操作轉發到偏函數傳回的對象上。
檢視這幾個函數的實作: def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError( 'working outside of request context')
return getattr(top , name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError( 'working outside of application context')
return getattr(top , name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError( 'working outside of application context')
return top.app
由于_app_ctx_stack和_request_ctx_stack都是線程隔離的,是以對g的調用就是這樣一個過程:
通路g-->從目前線程的應用上下文棧頂擷取應用上下文-->取出其中的g對象-->進行操作。 是以可以通過一個g對象而讓所有線程互不幹擾的通路自己的g。
上下文對象的推送
建構Flask對象後并不會推送上下文,而在Flask對象調用run()作為WSGI 應用啟動後,每當有請求進入時,在推送請求上下文前,如果有必要就會推送應用上下文。但運作了run就會阻塞程式,是以在shell中調試時,必須手動推送上下文;或者使用flask-scripts,它運作的任務會在開始時自動推送。
上面加粗的“如果有必要”,那麼什麼叫有必要呢?是不是意味着在每個線程裡應用上下文隻會被推送一次、一次請求結束下一次請求來的時候就不用再推送應用上下文了呢? 來看RequestContext的源碼,push函數: def push(self ): top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
# Before we push the request context we have to ensure that there
# is an application context.
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append( None)
_request_ctx_stack.push( self)
flask在推送請求上下文的時候調用push函數,他會檢查目前線程的應用上下文棧頂是否有應用上下文;如果有,判斷與請求上下文是否屬于同一個應用。在單WSGI應用的程式中,後者的判斷無意義。 此時,隻要沒有應用上下文就會推送一個目前應用的上下文,并且把該上下文記錄下來。
請求處理結束,調用auto_pop函數,其中又調用自身的pop函數: def pop(self, exc=None ): app_ctx = self._implicit_app_ctx_stack.pop() ……………… ……………… rv = _request_ctx_stack.pop() # Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)
會把請求上下文和應用上下文都pop掉。
故,在單WSGI應用環境下,每個請求的兩個上下文都是完全獨立的 (獨立于線程上曾經的請求,獨立于其他線程的請求)。Q.E.D
那麼,什麼時候沒必要推送呢?事實上,每次請求到來的時候都會推送,都是有必要的。因為當Flask在作為WSGI應用運作的時候,不可能出現目前線程的應用上下文已存在的情況。
那麼就要搞清什麼時候會有已存在的應用上下文。
研究時我參考了博文:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/ 該博文在最後提到了“兩個疑問”:①應用和請求上下文在運作時都是線程隔離的,為何要分開來?②每個線程同時隻處理一個請求,上下文棧肯定隻有一個對象,為何要用棧來存儲? 部落客認為,這兩個設計都是為了在離線狀态下調試用:
是以,綜上所述,在非離線狀态下,上下文棧在每個WSGI應用裡是獨立的,而每個應用裡線程同時隻處理一個請求,故上下文棧肯定隻有一個對象。并且,在請求結束後都會釋放,是以新的請求來的時候都會重新推送兩個上下文。
小結: 解釋了這麼多,對于flask程式設計來說,隻有一個應用上的結論:每個請求的g都是獨立的,并且在整個請求内都是可通路修改的。
ps. 本來隻是想知道能否在請求中儲存一個變量,就研究了g的生存周期和作用範圍,最後花了5個小時左右讀了flask英文文檔、各種博文和源代碼,寫了這些文字。我這對于細枝末節的東西吹毛求疵的精神真是害苦了我。。。
附上一些其他的筆記:
全局變量g,會在每次請求到來時重置
flask.
g
Just store on this whatever you want. For example a database connection or the user that is currently logged in.
Starting with Flask 0.10 this is stored on the application context and no longer on the request context which means it becomes available if only the application context is bound and not yet a request 這個意思是g在應用上下文,而不是請求上下文。隻要push了應用上下文就可以使用g對象 不要誤解為g是整個程式内共享的
The application context is created and destroyed as necessary. It never moves between threads and it will not be shared between requests.
推送程式上下文:app = Flask(xxx), app.app_context().push() 推送了程式上下文,g可以使用,目前線程的current_app指向app