flask 原始碼解析:上下文

發表於2017-02-21

這是 flask 原始碼解析系列文章的其中一篇,本系列所有文章列表:

上下文(application context 和 request context)

上下文一直是計算機中難理解的概念,在知乎的一個問題下面有個很通俗易懂的回答:

每一段程式都有很多外部變數。只有像Add這種簡單的函式才是沒有外部變數的。一旦你的一段程式有了外部變數,這段程式就不完整,不能獨立執行。你為了使他們執行,就要給所有的外部變數一個一個寫一些值進去。這些值的集合就叫上下文。
— vzch

比如,在 flask 中,檢視函式需要知道它執行情況的請求資訊(請求的 url,引數,方法等)以及應用資訊(應用中初始化的資料庫等),才能夠正確執行。

最直觀地做法是把這些資訊封裝成一個物件,作為引數傳遞給檢視函式。但是這樣的話,所有的檢視函式都需要新增對應的引數,即使該函式內部並沒有使用到它。

flask 的做法是把這些資訊作為類似全域性變數的東西,檢視函式需要的時候,可以使用 from flask import request 獲取。但是這些物件和全域性變數不同的是——它們必須是動態的,因為在多執行緒或者多協程的情況下,每個執行緒或者協程獲取的都是自己獨特的物件,不會互相干擾。

那麼如何實現這種效果呢?如果對 python 多執行緒比較熟悉的話,應該知道多執行緒中有個非常類似的概念 threading.local,可以實現多執行緒訪問某個變數的時候只看到自己的資料。內部的原理說起來也很簡單,這個物件有一個字典,儲存了執行緒 id 對應的資料,讀取該物件的時候,它動態地查詢當前執行緒 id 對應的資料。flaskpython 上下文的實現也類似,後面會詳細解釋。

flask 中有兩種上下文:application contextrequest context。上下文有關的內容定義在 globals.py 檔案,檔案的內容也非常短:

flask 提供兩種上下文:application contextrequest contextapp lication context 又演化出來兩個變數 current_appg,而 request context 則演化出來 requestsession

這裡的實現用到了兩個東西:LocalStackLocalProxy。它們兩個的結果就是我們可以動態地獲取兩個上下文的內容,在併發程式中每個檢視函式都會看到屬於自己的上下文,而不會出現混亂。

LocalStackLocalProxy 都是 werkzeug 提供的,定義在 local.py 檔案中。在分析這兩個類之前,我們先介紹這個檔案另外一個基礎的類 LocalLocal 就是實現了類似 threading.local 的效果——多執行緒或者多協程情況下全域性變數的隔離效果。下面是它的程式碼:

可以看到,Local 物件內部的資料都是儲存在 __storage__ 屬性的,這個屬性變數是個巢狀的字典:map[ident]map[key]value。最外面字典 key 是執行緒或者協程的 identity,value 是另外一個字典,這個內部字典就是使用者自定義的 key-value 鍵值對。使用者訪問例項的屬性,就變成了訪問內部的字典,外面字典的 key 是自動關聯的。__ident_func 是 協程的 get_current 或者執行緒的 get_ident,從而獲取當前程式碼所線上程或者協程的 id。

除了這些基本操作之外,Local 還實現了 __release_local__ ,用來清空(析構)當前執行緒或者協程的資料(狀態)。__call__ 操作來建立一個 LocalProxy 物件,LocalProxy 會在下面講到。

理解了 Local,我們繼續回來看另外兩個類。

LocalStack 是基於 Local 實現的棧結構。如果說 Local 提供了多執行緒或者多協程隔離的屬性訪問,那麼 LocalStack 就提供了隔離的棧訪問。下面是它的實現程式碼,可以看到它提供了 pushpoptop 方法。

__release_local__ 可以用來清空當前執行緒或者協程的棧資料,__call__ 方法返回當前執行緒或者協程棧頂元素的代理物件。

我們在之前看到了 request context 的定義,它就是一個 LocalStack 的例項:

它會當前執行緒或者協程的請求都儲存在棧裡,等使用的時候再從裡面讀取。至於為什麼要用到棧結構,而不是直接使用 Local,我們會在後面揭曉答案,你可以先思考一下。

LocalProxy 是一個 Local 物件的代理,負責把所有對自己的操作轉發給內部的 Local 物件。LocalProxy 的建構函式介紹一個 callable 的引數,這個 callable 呼叫之後需要返回一個 Local 例項,後續所有的屬性操作都會轉發給 callable 返回的物件。

這裡實現的關鍵是把通過引數傳遞進來的 Local 例項儲存在 __local 屬性中,並定義了 _get_current_object() 方法獲取當前執行緒或者協程對應的物件。

NOTE:前面雙下劃線的屬性,會儲存到 _ClassName__variable 中。所以這裡通過 “_LocalProxy__local” 設定的值,後面可以通過 self.__local 來獲取。關於這個知識點,可以檢視 stackoverflow 的這個問題

然後 LocalProxy 重寫了所有的魔術方法(名字前後有兩個下劃線的方法),具體操作都是轉發給代理物件的。這裡只給出了幾個魔術方法,感興趣的可以檢視原始碼中所有的魔術方法。

繼續回到 request context 的實現:

再次看這段程式碼希望能看明白,_request_ctx_stack 是多執行緒或者協程隔離的棧結構,request 每次都會呼叫 _lookup_req_object 棧頭部的資料來獲取儲存在裡面的 requst context

那麼請求上下文資訊是什麼被放在 stack 中呢?還記得之前介紹的 wsgi_app() 方法有下面兩行程式碼嗎?

每次在呼叫 app.__call__ 的時候,都會把對應的請求資訊壓棧,最後執行完請求的處理之後把它出棧。

我們來看看request_context, 這個 方法只有一行程式碼:

它呼叫了 RequestContext,並把 self 和請求資訊的字典 environ 當做引數傳遞進去。追蹤到 RequestContext 定義的地方,它出現在 ctx.py 檔案中,程式碼如下:

每個 request context 都儲存了當前請求的資訊,比如 request 物件和 app 物件。在初始化的最後,還呼叫了 match_request 實現了路由的匹配邏輯

push 操作就是把該請求的 ApplicationContext(如果 _app_ctx_stack 棧頂不是當前請求所在 app ,需要建立新的 app context) 和 RequestContext 有關的資訊儲存到對應的棧上,壓棧後還會儲存 session 的資訊; pop 則相反,把 request context 和 application context 出棧,做一些清理性的工作。

到這裡,上下文的實現就比較清晰了:每次有請求過來的時候,flask 會先建立當前執行緒或者程式需要處理的兩個重要上下文物件,把它們儲存到隔離的棧裡面,這樣檢視函式進行處理的時候就能直接從棧上獲取這些資訊。

NOTE:因為 app 例項只有一個,因此多個 request 共享了 application context

到這裡,關於 context 的實現和功能已經講解得差不多了。還有兩個疑惑沒有解答。

  1. 為什麼要把 request context 和 application context 分開?每個請求不是都同時擁有這兩個上下文資訊嗎?
  2. 為什麼 request context 和 application context 都有實現成棧的結構?每個請求難道會出現多個 request context 或者 application context 嗎?

第一個答案是“靈活度”,第二個答案是“多 application”。雖然在實際執行中,每個請求對應一個 request context 和一個 application context,但是在測試或者 python shell 中執行的時候,使用者可以單獨建立 request context 或者 application context,這種靈活度方便使用者的不同的使用場景;而且棧可以讓 redirect 更容易實現,一個處理函式可以從棧中獲取重定向路徑的多個請求資訊。application 設計成棧也是類似,測試的時候可以新增多個上下文,另外一個原因是 flask 可以多個 application 同時執行:

這個例子就是使用 werkzeugDispatcherMiddleware 實現多個 app 的分發,這種情況下 _app_ctx_stack 棧裡會出現兩個 application context。

參考資料

請使用手機”掃一掃”x

相關文章