本文章粘貼自 https://blog.tonyseek.com/post/the-context-mechanism-of-flask/數據庫
用過 Flask 作 Web 開發的同窗應該不會不記得 App Context 和 Request Context 這兩個名字——這兩個 Context 算是 Flask 中比較特點的設計。[1]flask
從一個 Flask App 讀入配置並啓動開始,就進入了 App Context,在其中咱們能夠訪問配置文件、打開資源文件、經過路由規則反向構造 URL。[2] 當一個請求進入開始被處理時,就進入了 Request Context,在其中咱們能夠訪問請求攜帶的信息,好比 HTTP Method、表單域等。[3]網絡
因此,這兩個 Context 也成了 Flask 框架複雜度比較集中的地方,對此有評價認爲 Flask 的這種設計比 Django、Tornado 等框架的設計更爲晦澀。[4] 我不認同這種評價。對於一個 Web 應用來講,「應用」 和 「請求」 的兩級上下文在理念上是現實存在的,若是理解了它們,那麼使用 Flask 並不會晦澀;即便是使用 Django、Tornado,理解了它們的 Context 也很是有利於作比官網例子更多的事情(例如編寫 Middleware)。數據結構
我由於開發 Flask 擴展,對這兩個 Context 的具體實現也研究了一番,同時還解決了一些本身以前「知道結論不知道過程」的疑惑,因此撰寫本文記錄下來。多線程
從面向對象設計的角度看,對象是保存「狀態」的地方。Python 也是如此,一個對象的狀態都被保存在對象攜帶的一個特殊字典中,能夠經過 vars 函數拿到它。app
Thread Local 則是一種特殊的對象,它的「狀態」對線程隔離 —— 也就是說每一個線程對一個 Thread Local 對象的修改都不會影響其餘線程。這種對象的實現原理也很是簡單,只要以線程的 ID 來保存多份狀態字典便可,就像按照門牌號隔開的一格一格的信箱。框架
在 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) # 這這個線程裏已經修改了 >>> >>> another = AnotherThread() >>> another.start() 2 >>> print(storage.foo) # 可是在主線程裏並無修改 1
Werkzeug 沒有直接使用 threading.local,而是本身實現了 werkzeug.local.Local 類。後者和前者有一些區別:ide
除 Local 外,Werkzeug 還實現了兩種數據結構:LocalStack 和 LocalProxy。函數
LocalStack 是用 Local 實現的棧結構,能夠將對象推入、彈出,也能夠快速拿到棧頂對象。固然,全部的修改都只在本線程可見。和 Local 同樣,LocalStack 也一樣實現了支持 release_pool 的接口。
LocalProxy 則是一個典型的代理模式實現,它在構造時接受一個 callable 的參數(好比一個函數),這個參數被調用後的返回值自己應該是一個 Thread Local 對象。對一個 LocalProxy 對象的全部操做,包括屬性訪問、方法調用(固然方法調用就是屬性訪問)甚至是二元操做 [6] 都會轉發到那個 callable 參數返回的 Thread Local 對象上。
LocalProxy 的一個使用場景是 LocalStack 的 __call__ 方法。好比 my_local_stack 是一個 LocalStack 實例,那麼 my_local_stack() 能返回一個 LocalProxy 對象,這個對象始終指向 my_local_stack 的棧頂元素。若是棧頂元素不存在,訪問這個 LocalProxy 的時候會拋出 RuntimeError。
Flask 是一個基於 Werkzeug 實現的框架,因此 Flask 的 App Context 和 Request Context 也理所固然地基於 Werkzeug 的 Local Stack 實現。
在概念上,App Context 表明了「應用級別的上下文」,好比配置文件中的數據庫鏈接信息;Request Context 表明了「請求級別的上下文」,好比當前訪問的 URL。
這兩種上下文對象的類定義在 flask.ctx 中,它們的用法是推入 flask.globals 中建立的 _app_ctx_stack 和 _request_ctx_stack 這兩個單例 Local Stack 中。由於 Local Stack 的狀態是線程隔離的,而 Web 應用中每一個線程(或 Greenlet)同時只處理一個請求,因此 App Context 對象和 Request Context 對象也是請求間隔離的。
當 app = Flask(__name__) 構造出一個 Flask App 時,App Context 並不會被自動推入 Stack 中。因此此時 Local Stack 的棧頂是空的,current_app 也是 unbound 狀態。
>>> 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 用戶可能被坑的地方 —— 好比編寫一個離線腳本時,若是直接在一個 Flask-SQLAlchemy 寫成的 Model 上調用 User.query.get(user_id),就會遇到 RuntimeError。由於此時 App Context 還沒被推入棧中,而 Flask-SQLAlchemy 須要數據庫鏈接信息時就會去取 current_app.config,current_app 指向的倒是 _app_ctx_stack 爲空的棧頂。
解決的辦法是運行腳本正文以前,先將 App 的 App Context 推入棧中,棧頂不爲空後 current_app 這個 Local Proxy 對象就天然能將「取 config 屬性」 的動做轉發到當前 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>
那麼爲何在應用運行時不須要手動 app_context().push() 呢?由於 Flask App 在做爲 WSGI Application 運行時,會在每一個請求進入的時候將請求上下文推入 _request_ctx_stack 中,而請求上下文必定是 App 上下文之中,因此推入部分的邏輯有這樣一條:若是發現 _app_ctx_stack爲空,則隱式地推入一個 App 上下文。
因此,請求中是不須要手動推上下文入棧的,可是離線腳本須要手動推入 App Context。若是沒有什麼特殊困難,我更建議用 Flask-Script 來寫離線任務。[7]
到此爲止,就出現兩個疑問:
我最初也被這兩個疑問困惑過。後來看了一些資料,就明白了 Flask 爲什麼要設計成這樣。這兩個作法給予咱們 多個 Flask App 共存 和 非 Web Runtime 中靈活控制 Context 的可能性。
咱們知道對一個 Flask App 調用 app.run() 以後,進程就進入阻塞模式並開始監聽請求。此時是不可能再讓另外一個 Flask App 在主線程運行起來的。那麼還有哪些場景須要多個 Flask App 共存呢?前面提到了,一個 Flask App 實例就是一個 WSGI Application,那麼 WSGI Middleware 是容許使用組合模式的,好比:
from werkzeug.wsgi import DispatcherMiddleware from biubiu.app import create_app from biubiu.admin.app import create_app as create_admin_app application = DispatcherMiddleware(create_app(), { '/admin': create_admin_app() })
這個例子就利用 Werkzeug 內置的 Middleware 將兩個 Flask App 組合成一個一個 WSGI Application。這種狀況下兩個 App 都同時在運行,只是根據 URL 的不一樣而將請求分發到不一樣的 App 上處理。
Note
須要注意的是,這種用法和 Flask 的 Blueprint 是有區別的。Blueprint 雖然和這種用法很相似,但前者本身沒有 App Context,只是同一個 Flask App 內部整理資源的一種方式,因此多個 Blueprint 可能共享了同一個 Flask App;後者面向的是全部 WSGI Application,而不只僅是 Flask App,即便是把一個 Django App 和一個 Flask App 用這種用法整合起來也是可行的。
若是僅僅在 Web Runtime 中,多個 Flask App 同時工做倒不是問題。畢竟每一個請求被處理的時候是身處不一樣的 Thread Local 中的。可是 Flask App 不必定僅僅在 Web Runtime 中被使用 —— 有兩個典型的場景是在非 Web 環境須要訪問上下文代碼的,一個是離線腳本(前面提到過),另外一個是測試。這兩個場景即所謂的「Running code outside of a request」。
離線腳本或者測試這類非 Web 環境和和 Web 環境不一樣 —— 前者通常只在主線程運行。
設想,一個離線腳本須要操做兩個 Flask App 關聯的上下文,應該怎麼辦呢?這時候棧結構的 App Context 優點就發揮出來了。
from biubiu.app import create_app from biubiu.admin.app import create_app as create_admin_app app = create_app() admin_app = create_admin_app() def copy_data(): with app.app_context(): data = read_data() # fake function for demo with admin_app.app_context(): write_data(data) # fake function for demo mark_data_copied() # fake function for demo
不管有多少個 App,只要主動去 Push 它的 App Context,Context Stack 中就會累積起來。這樣,棧頂永遠是當前操做的 App Context。當一個 App Context 結束的時候,相應的棧頂元素也隨之出棧。若是在執行過程當中拋出了異常,對應的 App Context 中註冊的 teardown 函數被傳入帶有異常信息的參數。
這麼一來就解釋了兩個疑問 —— 在這種單線程運行環境中,只有棧結構才能保存多個 Context 並在其中定位出哪一個纔是「當前」。而離線腳本只須要 App 關聯的上下文,不須要構造出請求,因此 App Context 也應該和 Request Context 分離。
另外一個手動推入 Context 的場景是測試。測試中咱們可能會須要構造一個請求,並驗證相關的狀態是否符合預期。例如:
def test_app(): app = create_app() client = app.test_client() resp = client.get('/') assert 'Home' in resp.data
這裏調用 client.get 時,Request Context 就被推入了。其特色和 App Context 很是相似,這裏再也不贅述。
[1] | Flask 文檔對 Application Context 和 Request Context 做出了詳盡的解釋; |
[2] | 經過訪問 flask.current_app; |
[3] | 經過訪問 flask.request; |
[4] | Flask(Werkzeug) 的 Context 基於 Thread Local 和代理模式實現,只要身處 Context 中就能用近似訪問全局變量的的方式訪問到上下文信息,例如 flask.current_app 和 flask.request;Django 和 Tornado 則將上下文封裝在對象中,只有明確獲取了相關上下文對象才能訪問其中的信息,例如在視圖函數中或按照規定模板實現的 Middleware 中; |
[5] | 基於 Flask 的 Web 應用能夠在 Gevent 或 Eventlet 異步網絡庫 patch 過的 Python 環境中正常工做。這兩者都使用 Greenlet 而不是系統線程做爲調度單元,而 Werkzeug 考慮到了這點,在 Greenlet 可用時用 Greenlet ID 代替線程 ID。 |
[6] | Python 的對象方法是 Descriptior 實現的,因此方法就是一種屬性;而 Python 的二元操做能夠用雙下劃線開頭和結尾的一系列協議,因此 foo + bar 等同於 foo.__add__(bar),本質仍是屬性訪問。 |
[7] | Flask-Script 是一個用來寫 manage.py 管理腳本的 Flask 擴展,用它運行的任務會在開始前自動推入 App Context。未來這個「運行任務」的功能將被整合到 Flask 內部。 |
[8] | 詳見 Flask 源碼中的 setup_method 裝飾器 |
推送程序上下文:app = Flask(xxx), app.app_context().push() 推送了程序上下文,g可使用,當前線程的current_app指向app