Flask 中的 Context 初探#
大家新年好!鑑於今年春晚非常好看,我覺得承受不起,於是來寫點辣雞水文娛樂下大家,這也是之前立的若干 Flag 中的一個
正文#
做過 Flask 開發的朋友都知道 Flask 中存在著兩個概念,一個叫 App Context,一個叫 Request Context。這兩個算是 Flask 中很獨特的一種機制。
從一個 Flask App 讀入配置並啟動開始,就進入了 App Context,在其中我們可以訪問配置文件、打開資源文件、通過路由規則反向構造 URL。當 WSGI Middleware 調用 Flask App 的時候開始,就進入了 Request Context。我們可以獲取到其中的 HTTP HEADER 等操作,同時也可以進行 SESSION 等操作。
不過作為辣雞選手而言,經常分不清為什麼會存在這兩個 Context,沒事,我們慢慢來說一說。
預備知識#
首先要清楚一點,我們要在同一個進程中隔離不同線程的數據,那麼我們會優先選擇 threading.local
,來實現數據彼此隔離的需求。但是現在有個問題來了,現在我們的並發模型可能並不是只有傳統意義上的進程 - 線程模型。也有可能是 coroutine (協程) 模型。常見的就是 Greenlet/Eventlet。在這種情況下,threading.local
就沒法很好地滿足我們的需求。於是 Werkzeug 實現了自己的 Local 即 werkzeug.local.Local
那麼 Werkzeug 自己實現的 Local 和標準的 threading.local
相比有什麼不同呢?我們記住最大的不同點在於
前者會在 Greenlet 可用的情況下優先使用 Greenlet 的 ID 而不是線程 ID 以支持 Gevent 或 Eventlet 的調度,後者只支持多線程調度;
Werkzeug 另外還實現了兩種數據結構,一個叫 LocalStack
,一個叫做 LocalProxy
LocalStack
是基於 Local
實現的一個棧結構。棧的特性就是後入先出。當我們進入一個 Context 時,將當前的對象推入棧中。然後我們也可以獲取到棧頂元素。從而獲取到當前的上下文信息。
LocalProxy
是代理模式的一種實現。在實例化的時候,傳入一個 callable
的參數。然後這個參數被調用後將會返回一個 Local
對象。我們後續的所有操作,比如屬性調用,數值計算等,都会轉發到這個參數返回的 Local
對象上。
現在大家可能不太清楚,我們為什麼要用 LocalProxy 來進行操作,我們來給大家看一個例子
from werkzeug.local import LocalStack
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})
def get_item():
return test_stack.pop()
item = get_item()
print(item['abc'])
print(item['abc'])
你看我們這裡的輸出的值,都是統一的 1234
,但是我們這裡想做到的是每次獲取的值都是棧頂的最新的元素,那麼我們這個時候就應該用 proxy 模式了
from werkzeug.local import LocalStack, LocalProxy
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})
def get_item():
return test_stack.pop()
item = LocalProxy(get_item)
print(item['abc'])
print(item['abc'])
你看我們這裡就是 Proxy 的妙用。
Context#
由於 Flask 基於 Werkzeug 實現,因此 App Context 以及 Request Context 是基於前文中所說的 LocalStack 實現。
從命名上,大家應該可以看出,App Context 是代表應用上下文,可能包含各種配置信息,比如日誌配置,數據庫配置等。而 Request Context 代表一個請求上下文,我們可以獲取到當前請求中的各種信息。比如 body 攜帶的信息。
這兩個上下文的定義是在 flask.ctx 文件中,分別是 AppContext
以及 RequestContext
。而構建上下文的操作則是將其推入在 flask.globals 文件中定義的 _app_ctx_stack
以及 _request_ctx_stack
中。前面說了 LocalStack 是 “線程”(這裡可能是傳統意義上的線程,也有可能是 Greenlet 這種)隔離的。同時 Flask 每個線程只處理一個請求,因此可以做到請求隔離。
當 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>
作為 web 時,當請求進來時,我們開始進行上下文的相關操作。整個流程如下:
好了現在有點問題:
-
為什麼要區分 App Context 以及 Request Context
-
為什麼要用棧結構來實現 Context?
很久之前看過的松鼠奧利奧老師的博文Flask 的 Context 機制 解答了這個問題
這兩個做法給予我們 多個 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 上處理。
但是現在很多朋友有個問題,就是為什麼這裡不用 Blueprint?
-
Blueprint 是在同一個 App 下運行。其掛在 App Context 上的相關信息都是一致的。但是如果要隔離彼此的信息的話,那麼用 App Context 進行隔離,會比我們用變量名什麼的隔離更為方便
-
Middleware 模式是 WSGI 中允許的特性,換句話來講,我們將 Flask 和另外一個遵循 WSGI 協議的 web Framework(比如 Django)那麼也是可行的。
但是 Flask 的兩種 Context 分離更大的意義是為了非 web 應用的場合。Flask 官方文檔中有這樣一段話
The main reason for the application’s context existence is that in the past a bunch of functionality was attached to the request context for lack of a better solution. Since one of the pillars of Flask’s design is that you can have more than one application in the same Python process.
這句話換句話說 App Context 存在的意義是針對一個進程中有多個 Flask App 場景,這樣場景最常見的就是我們用 Flask 來做一些離線腳本的代碼。
好了,我們來聊聊 Flask 非 Web 應用的場景
比如,我們有個插件叫 Flask-SQLAlchemy
然後這裡有個使用場景
首先我們現在有這樣一段代碼
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(database)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
這裡你應該注意到最開始的幾個關鍵點,第一個,就是 database.config
,是的沒錯,Flask-SQLAlchemy 就是從當前的 app 中獲取到對應的 config 信息來建立數據庫鏈接。那麼傳遞 app 的方式有兩種,第一種,就是直接如上圖一樣,直接 db = SQLAlchemy (database),這個很容易理解,第二種,如果我們不傳的話,那麼 Flask-SQLAlchemy 中通過 current_app 來獲取當前的 app 然後獲取對應的 config 建立鏈接。
那麼問題來了,為什麼會存在第二種這種方法呢
給個場景吧,現在我兩個數據庫配置不同的 app 共用一個 Model 那麼應該怎麼做?其實很簡單
首先寫 一個 model 文件,比如就叫 data/user_model.py 吧
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
好了,那麼在我們的應用文件中,我們便可以這樣寫
from data.user_model import User
database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
with database.app_context():
db.init_app(current_app)
db.create_all()
admin = User(username='admin', email='[email protected]')
db.session.add(admin)
db.session.commit()
print(User.query.filter_by(username="admin").first())
database1 = Flask(__name__)
database1.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test1.db'
with database1.app_context():
db.init_app(current_app)
db.create_all()
admin = User(username='admin_test', email='[email protected]')
db.session.add(admin)
db.session.commit()
print(User.query.filter_by(username="admin").first())
你看這樣是不是就好懂了一些,通過 app context,我們 Flask-SQLAlchemy 可以通過 current_app 來獲取當前 app,繼而獲取相關的 config 信息
這個例子還不夠妥當,我們現在再來換一個例子
from flask import Flask, current_app
import logging
app = Flask("app1")
app2 = Flask("app2")
app.config.logger = logging.getLogger("app1.logger")
app2.config.logger = logging.getLogger("app2.logger")
app.logger.addHandler(logging.FileHandler("app_log.txt"))
app2.logger.addHandler(logging.FileHandler("app2_log.txt"))
with app.app_context():
with app2.app_context():
try:
raise ValueError("app2 error")
except Exception as e:
current_app.config.logger.exception(e)
try:
raise ValueError("app1 error")
except Exception as e:
current_app.config.logger.exception(e)
好了,這段代碼很清晰了,含義很清晰,就是通過獲取當前上下文中的 app 中的 logger 來輸出日誌。同時這段代碼也很清晰的說明了,我們為什麼要用棧這樣一種數據結構來維護上下文。
首先看一下 app_context()
的源碼
def app_context(self):
"""Binds the application only. For as long as the application is bound
to the current context the :data:`flask.current_app` points to that
application. An application context is automatically created when a
request context is pushed if necessary.
Example usage::
with app.app_context():
...
.. versionadded:: 0.9
"""
return AppContext(self)
嗯,很簡單,只是構建一個 AppContext 對象返回,然後我們看看相關的代碼
class AppContext(object):
"""The application context binds an application object implicitly
to the current thread or greenlet, similar to how the
:class:`RequestContext` binds request information. The application
context is also implicitly created if a request context is created
but the application is not on top of the individual application
context.
"""
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
def push(self):
"""Binds the app context to the current context."""
self._refcnt += 1
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_app_ctx_stack.push(self)
appcontext_pushed.send(self.app)
def pop(self, exc=_sentinel):
"""Pops the app context."""
try:
self._refcnt -= 1
if self._refcnt <= 0:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
finally:
rv = _app_ctx_stack.pop()
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
% (rv, self)
appcontext_popped.send(self.app)
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
self.pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
emmmm,首先 push
方法就是將自己推入 _app_ctx_stack
,而 pop
方法則是將自己從棧頂推出。然後我們看到兩個方法含義就很明確了,在進入上下文管理器的時候,將自己推入棧,然後退出上下文管理器的時候,將自己推出。
我們都知道棧的一個性質就是,後入先出,棧頂的永遠是最新插入進去的元素。而看一下我們 current_app
的源碼
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
current_app = LocalProxy(_find_app)
嗯,很明了了,就是獲取當前棧頂的元素,然後進行相關操作。
嗯,通過這樣對於棧的不斷操作,就能讓 current_app
獲取到元素是我們當前上下文中的 app 。
額外的講解: g#
g 也是我們常用的幾個全局變量之一。在最開始這個變量是掛載在 Request Context 下的。但是在 0.10 以後,g 就是掛載在 App Context 下的。可能有同學不太清楚為什麼要這麼做。
首先,說一下 g 用來幹什麼
官方在上下文這一章裡有這一段說明
The application context is created and destroyed as necessary. It never moves between threads and it will not be shared between requests. As such it is the perfect place to store database connection information and other things. The internal stack object is called flask._app_ctx_stack. Extensions are free to store additional information on the topmost level, assuming they pick a sufficiently unique name and should put their information there, instead of on the flask.g object which is reserved for user code.
大意就是說,數據庫配置和其餘的重要配置信息,就掛載 App 對象上。但是如果是一些用戶代碼,比如你不想一層層函數傳數據的話,然後有一些變量需要傳遞,那麼可以掛在 g 上。
同時前面說了,Flask 並不僅僅可以當做一個 Web Framework 使用,同時也可以用於一些非 web 的場合下。在這種情況下,如果 g 是屬於 Request Context 的話,那麼我們要使用 g 的話,那麼就需要手動構建一個請求,無疑是不合理的。
最後#
大年三十寫這篇文章,現在發出來,我的辣雞也是無人可救了。Flask 的上下文機制是其最重要的特性之一。通過合理的利用上下文機制,我們可以在更多的場合下去更好的利用 flask。嗯,本次的辣雞文章寫作活動就到此結束吧。希望大家不會扔我臭雞蛋!然後新年快樂!