Flask中的請求上下文和應用上下文

發表於2017-05-04

在Flask中處理請求時,應用會生成一個“請求上下文”物件。整個請求的處理過程,都會在這個上下文物件中進行。這保證了請求的處理過程不被干擾。處理請求的具體程式碼如下:

在Flask 0.9版本之前,應用只有“請求上下文”物件,它包含了和請求處理相關的資訊。同時Flask還根據werkzeug.local模組中實現的一種資料結構LocalStack用來儲存“請求上下文”物件。這在{% post_link 一個Flask應用執行過程剖析 一個Flask應用執行過程剖析 %}中有所介紹。在0.9版本中,Flask又引入了“應用上下文”的概念。本文主要Flask中的這兩個“上下文”物件。

LocalStack

在介紹“請求上下文”和“應用上下文”之前,我們對LocalStack簡要做一個回顧。在Werkzeug庫——local模組一文中,我們講解了werkzeug.local模組中實現的三個類LocalLocalStackLocalProxy。關於它們的概念和詳細介紹,可以檢視上面的文章。這裡,我們用一個例子來說明Flask中使用的一種資料結構LocalStack

由上面的例子可以看出,儲存在LocalStack中的資訊以字典的形式存在:鍵為執行緒/協程的標識數值,值也是字典形式。每當有一個執行緒/協程上要將一個物件pushLocalStack棧中,會形成如上一個“鍵-值”對。這樣的一種結構很好地實現了執行緒/協程的隔離,每個執行緒/協程都會根據自己執行緒/協程的標識數值確定儲存在棧結構中的值。

LocalStack還實現了pushpoptop等方法。其中top方法永遠指向棧頂的元素。棧頂的元素是指當前執行緒/協程中最後被推入棧中的元素,即local_stack._local.stack[-1](注意,是stack鍵對應的物件中最後被推入的元素)。

請求上下文

Flask中所有的請求處理都在“請求上下文”中進行,在它設計之初便就有這個概念。由於0.9版本程式碼比較複雜,這裡還是以0.1版本的程式碼為例進行說明。本質上這兩個版本的“請求上下文”的執行原理沒有變化,只是新版本增加了一些功能,這點在後面再進行解釋。

請求上下文——0.1版本

由上面“請求上下文”的實現可知:

  • “請求上下文”是一個上下文物件,實現了__enter____exit__方法。可以使用with語句構造一個上下文環境。
  • 進入上下文環境時,_request_ctx_stack這個棧中會推入一個_RequestContext物件。這個棧結構就是上面講的LocalStack棧。
  • 推入棧中的_RequestContext物件有一些屬性,包含了請求的的所有相關資訊。例如apprequestsessiongflashes。還有一個url_adapter,這個物件可以進行URL匹配。
  • with語句構造的上下文環境中可以進行請求處理。當退出上下文環境時,_request_ctx_stack這個棧會銷燬剛才儲存的上下文物件。

以上的執行邏輯使得請求的處理始終在一個上下文環境中,這保證了請求處理過程不被干擾,而且請求上下文物件儲存在LocalStack棧中,也很好地實現了執行緒/協程的隔離。

以下是一個簡單的例子:

上面的結果顯示:_request_ctx_stack中為每一個執行緒建立了一個“鍵-值”對,每一“鍵-值”對中包含一個請求上下文物件。如果使用with語句,在離開上下文環境時棧中銷燬儲存的上下文物件資訊。

請求上下文——0.9版本

在0.9版本中,Flask引入了“應用上下文”的概念,這對“請求上下文”的實現有一定的改變。這個版本的“請求上下文”也是一個上下文物件。在使用with語句進入上下文環境後,_request_ctx_stack會儲存這個上下文物件。不過與0.1版本相比,有以下幾點改變:

  • 請求上下文實現了pushpop方法,這使得對於請求上下文的操作更加的靈活;
  • 伴隨著請求上下文物件的生成並儲存在棧結構中,Flask還會生成一個“應用上下文”物件,而且“應用上下文”物件也會儲存在另一個棧結構中去。這是兩個版本最大的不同。

我們先看一下0.9版本相關的程式碼:

我們注意到,0.9版本的“請求上下文”的pop方法中,當要將一個“請求上下文”推入_request_ctx_stack棧中的時候,會先檢查另一個棧_app_ctx_stack的棧頂是否存在“應用上下文”物件或者棧頂的“應用上下文”物件的應用是否是當前應用。如果不存在或者不是當前物件,Flask會自動先生成一個“應用上下文”物件,並將其推入_app_ctx_stack中。

我們再看離開上下文時的相關程式碼:

上面程式碼中的細節先不討論。注意到當要離開以上“請求上下文”環境的時候,Flask會先將“請求上下文”物件從_request_ctx_stack棧中銷燬,之後會根據實際的情況確定銷燬“應用上下文”物件。

以下還是以一個簡單的例子進行說明:

應用上下文

上部分中簡單介紹了“應用上下文”和“請求上下文”的關係。那什麼是“應用上下文”呢?我們先看一下它的類:

由以上程式碼可以看出:“應用上下文”也是一個上下文物件,可以使用with語句構造一個上下文環境,它也實現了pushpop等方法。“應用上下文”的建構函式也和“請求上下文”類似,都有appurl_adapter等屬性。“應用上下文”存在的一個主要功能就是確定請求所在的應用。

然而,以上的論述卻又讓人產生這樣的疑問:既然“請求上下文”中也包含app等和當前應用相關的資訊,那麼只要呼叫_request_ctx_stack.top.app或者魔法current_app就可以確定請求所在的應用了,那為什麼還需要“應用上下文”物件呢?對於單應用單請求來說,使用“請求上下文”確實就可以了。然而,Flask的設計理念之一就是多應用的支援。當在一個應用的請求上下文環境中,需要巢狀處理另一個應用的相關操作時,“請求上下文”顯然就不能很好地解決問題了。如何讓請求找到“正確”的應用呢?我們可能會想到,可以再增加一個請求上下文環境,並將其推入_request_ctx_stack棧中。由於兩個上下文環境的執行是獨立的,不會相互干擾,所以通過呼叫_request_ctx_stack.top.app或者魔法current_app也可以獲得當前上下文環境正在處理哪個應用。這種辦法在一定程度上可行,但是如果對於第二個應用的處理不涉及到相關請求,那也就無從談起“請求上下文”。

為了應對這個問題,Flask中將應用相關的資訊單獨拿出來,形成一個“應用上下文”物件。這個物件可以和“請求上下文”一起使用,也可以單獨拿出來使用。不過有一點需要注意的是:在建立“請求上下文”時一定要建立一個“應用上下文”物件。有了“應用上下文”物件,便可以很容易地確定當前處理哪個應用,這就是魔法current_app。在0.1版本中,current_app是對_request_ctx_stack.top.app的引用,而在0.9版本中current_app是對_app_ctx_stack.top.app的引用。

下面以一個多應用的例子進行說明:

在以上的例子中:

  • 我們首先建立了兩個Flask應用appapp2
  • 接著我們構建了一個app的請求上下文環境。當進入這個環境中時,這時檢視兩個棧的內容,發現兩個棧中已經有了當前請求的請求上下文物件和應用上下文物件。並且棧頂的元素都是app的請求上下文和應用上下文;
  • 之後,我們再在這個環境中巢狀app2的應用上下文。當進入app2的應用上下文環境時,兩個上下文環境便隔離開來,此時再檢視兩個棧的內容,發現_app_ctx_stack中推入了app2的應用上下文物件,並且棧頂指向它。這時在app2的應用上下文環境中,current_app便會一直指向app2
  • 當離開app2的應用上下文環境,_app_ctx_stack棧便會銷燬app2的應用上下文物件。這時檢視兩個棧的內容,發現兩個棧中只有app的請求的請求上下文物件和應用上下文物件。
  • 最後,離開app的請求上下文環境後,兩個棧便會銷燬app的請求的請求上下文物件和應用上下文物件,棧為空。

與上下文物件有關的“全域性變數”

在Flask中,為了更加方便地處理一些變數,特地提出了“全域性變數”的概念。這些全域性變數有:

可以看出,Flask中使用的一些“全域性變數”,包括current_apprequestsessiong等都來自於上下文物件。其中current_app一直指向_app_ctx_stack棧頂的“應用上下文”物件,是對當前應用的引用。而requestsessiong等一直指向_request_ctx_stack棧頂的“請求上下文”物件,分別引用請求上下文的requestsessiong。不過,從 Flask 0.10 起,物件 g 儲存在應用上下文中而不再是請求上下文中。

另外一個問題,在形成這些“全域性變數”的時候,使用了werkzeug.local模組的LocalProxy類。之所以要用該類,主要是為了動態地實現對棧頂元素的引用。如果不使用這個類,在生成上述“全域性變數”的時候,它們因為指向棧頂元素,而棧頂元素此時為None,所以這些變數也會被設定為None常量。後續即使有上下文物件被推入棧中,相應的“全域性變數”也不會發生改變。為了動態地實現對棧頂元素的引用,這裡必須使用werkzeug.local模組的LocalProxy類。

相關文章