Flask的上下文
flask 中有兩種上下文:application context 和 request context。上下文有關的內容定義在 globals.py 文件
# 關鍵處,後面會解釋
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
# 請求上下文棧對象
_request_ctx_stack = LocalStack()
# 應用上下文棧對象
_app_ctx_stack = LocalStack()
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'))
這裏的實現用到了兩個東西:LocalStack 和 LocalProxy。它們兩個的結果就是咱們能夠動態地獲取兩個上下文的內容,在併發程序中每一個視圖函數都會看到屬於本身的上下文,而不會出現混亂。
LocalStack 和 LocalProxy 都是 werkzeug 提供的,定義在 local.py 文件中,分析以前,先了解一個相似於threading.local 的效果的類Local,它實現了多線程或者多協程狀況下全局變量的隔離效果
try:
# 表示能夠處理多協程
from greenlet import getcurrent as get_ident
except ImportError:
try:
# 表示能夠處理多線程
from thread import get_ident
except ImportError:
from _thread import get_ident
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
# 數據保存在 __storage__ 中,產生了{'__storage__':{}}結構, 後續訪問都是對該屬性的操做
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
# 用來清空(析構)當前線程或者協程的數據(狀態)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)
# 下面三個方法實現了屬性的訪問、設置和刪除。
# 注意到,內部都調用 `self.__ident_func__` 獲取當前線程或者協程的 id,而後再訪問對應的內部字典。
# 若是訪問或者刪除的屬性不存在,會拋出 AttributeError。
# 這樣,外部用戶看到的就是它在訪問實例的屬性,徹底不知道字典或者多線程/協程切換的實現
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
`
上面的Local類的做用就是產生下面的數據結構
{'__storage__':{
'線程或者協程的 id':{name:value}
}}
`
瞭解了Local類後,回來分析LocalStack 和 LocalProxy
LocalStack 是基於 Local 實現的棧結構。若是說 Local 提供了多線程或者多協程隔離的屬性訪問,那麼 LocalStack 就提供了隔離的棧訪問
class LocalStack(object):
def __init__(self):
# 實例化Local類,並封裝到LocalStack類中
self._local = Local()
def __release_local__(self):
"""能夠用來清空當前線程或者協程的棧數據"""
self._local.__release_local__()
def __call__(self):
"""用於返回當前線程或者協程棧頂元素的代理對象。"""
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup)
# push、pop 和 top 三個方法實現了棧的操做,
# 能夠看到棧的數據是保存在 self._local.stack 屬性中的
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
stack = getattr(self._local, 'stack', None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
@property
def top(self):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
咱們能夠看到最上面有一行代碼,他就是實例化一個LocalStack對象:
# 實例化 請求上下文棧對象
_request_ctx_stack = LocalStack()
# 它會把當前線程或者協程的請求都保存在棧裏,等使用的時候再從裏面讀取
LocalProxy 是一個 Local 對象的代理,負責把全部對本身的操做轉發給內部的 Local 對象。
class LocalProxy(object):
__slots__ = ('__local', '__dict__', '__name__')
def __init__(self, local, name=None):
# 產生了{'__local':local對象}結構,爲何_LocalProxy__local會變成__local,請參考面向對象的私有屬性
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
def _get_current_object(self):
"""用於獲取當前線程或者協程對應的對象"""
if not hasattr(self.__local, '__release_local__'):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
def __setitem__(self, key, value):
self._get_current_object()[key] = value
初步瞭解了LocalStack和LocalProxy類後,還要了解一個小知識點:
偏函數:
import functools
# 偏函數
def func(a1,a2):
print(a1,a2)
# 幫func函數傳遞參數,併產生新的函數名
new_func = functools.partial(func,123)
# 新的函數只須要傳一個參數
new_func(2)
如今再看下面的代碼
_request_ctx_stack = LocalStack()
# 調用 _lookup_req_object方法,拿到封裝在 請求上下文 裏的request對象
request = LocalProxy(partial(_lookup_req_object, 'request'))
# 調用 _lookup_req_object方法,拿到封裝在 請求上下文 裏的session對象
session = LocalProxy(partial(_lookup_req_object, 'session'))
若是你在疑惑 :何時請求上下文裏封裝了request對象和session對象?那你可記得以前的wsgi_app方法:
def wsgi_app(self, environ, start_response):
# 先建立RequestContext對象,並封裝了request對象
ctx = self.request_context(environ)
error = None
try:
try:
# ctx.push的內部完成了什麼?
# 1. 先創建了_request_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
# 完成功能代碼: RequestContext/push方法裏的 top = _request_ctx_stack.top
# 2. 再建立_app_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
# 完成功能代碼: RequestContext/push方法裏的 app_ctx = _app_ctx_stack.top
# 3. 再建立AppContext對象並封裝了app和g
# 完成功能代碼: RequestContext/push方法裏的 app_ctx = self.app.app_context()
# 4. 把AppContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[app_ctx對象]}}}數據結構
# 完成功能代碼: app_ctx.push()
# 5. 把RequestContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[ctx對象]}}}數據結構
# 完成功能代碼: _request_ctx_stack.push(self)
# 6. RequestContext對象封裝session
# 完成功能代碼: self.session = session_interface.open_session(self.app, self.request)
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
ctx.push()的詳細分析:
# RequestContext類中push方法
def push(self):
#1. 先創建了_request_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
top = _request_ctx_stack.top
# 2. 再建立_app_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
# 3. 再建立AppContext對象並封裝了app和g
app_ctx = self.app.app_context()
# 4. 把AppContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[app_ctx對象]}}}數據結構
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
# 5. 把RequestContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[ctx對象]}}}數據結構
_request_ctx_stack.push(self)
if self.session is None:
session_interface = self.app.session_interface
# 6. RequestContext對象封裝session
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
if self.url_adapter is not None:
# 實現了路由的匹配邏輯
self.match_request()
首先從下面兩行代碼開始:
ctx = self.request_context(environ)
ctx.push()
每次在調用 app.__call__
的時候,都會把對應的請求信息壓棧,最後執行完請求的處理以後把它出棧。
咱們來看看request_context, 這個 方法只return一個類:
def request_context(self, environ):
# 調用了 RequestContext,並把 self 和請求信息的字典 environ 當作參數傳遞進去
return RequestContext(self, environ)
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.match_request()
def match_request(self):
try:
url_rule, self.request.view_args =
self.url_adapter.match(return_rule=True)
self.request.url_rule = url_rule
except HTTPException as e:
self.request.routing_exception = e
def push(self):
"""把該請求的 請求上下文和應用上下文 有關的信息保存到各自對應的棧上,具體看上面"""
top = _request_ctx_stack.top
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)
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
def pop(self, exc=_sentinel):
""" 和push相反,把 請求上下文和應用上下文 有關的信息從各自對應的棧上刪除"""
app_ctx = self._implicit_app_ctx_stack.pop()
try:
clear_request = False
if not self._implicit_app_ctx_stack:
self.app.do_teardown_request(exc)
request_close = getattr(self.request, 'close', None)
if request_close is not None:
request_close()
clear_request = True
finally:
rv = _request_ctx_stack.pop()
if clear_request:
rv.request.environ['werkzeug.request'] = None
if app_ctx is not None:
app_ctx.pop(exc)
def auto_pop(self, exc):
if self.request.environ.get('flask._preserve_context') or
(exc is not None and self.app.preserve_context_on_exception):
self.preserved = True
self._preserved_exc = exc
else:
self.pop(exc)
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
self.auto_pop(exc_value)
每一個 request context 都保存了當前請求的信息,好比 request 對象和 app 對象,初始化的最後調用了 match_request
實現了路由的匹配邏輯
push
操做就是把該請求的 ApplicationContext
(若是 _app_ctx_stack
棧頂不是當前請求所在 app ,須要建立新的 app context) 和 RequestContext
有關的信息保存到對應的棧上,壓棧後還會保存 session 的信息; pop
則相反,把 request context 和 application context 出棧,作一些清理性的工做。
結論:
`
每次有請求過來的時候,flask 會先建立當前線程或者進程須要處理的兩個重要上下文對象,把它們保存到隔離的棧裏面,這樣視圖函數進行處理的時候就能直接從棧上獲取這些信息
`
上面分析事後,你可能有三個疑惑:
# 1. 不會產生多個app嗎?
`不會,此處app對象運用了單例模式,因此只有一個app對象,所以多個 request 共享了 application context`
# 2. 爲何要把 請求上下文 和 應用上下文 分開?每一個請求不是都同時擁有這兩個上下文信息嗎?
`由於'靈活性',雖然在實際運行中,每一個請求對應一個 請求上下文 和一個 應用上下文,可是在測試或者 python shell 中運行的時候,用戶能夠單首創建 請求上下文 或者 應用上下文,這種靈活度方便用戶的不一樣的使用場景;`
# 3. 爲何 請求上下文 和 應用上下文 都有實現成棧的結構?每一個請求難道會出現多個 請求上下文 或者 應用上下文 嗎?
`在web runtime 時,棧永遠只有1個對象。可是在寫離線腳本時,纔會用在棧中放多個對象.(建立一個py文件本地運行)`
第三個問題的代碼示例:
# --------------------------例1------------------------------------
from flask import current_app,g
from pro_excel import create_app
app1 = create_app()
with app1.app_context(): # AppContext對象(app,g) -> local對象
print(current_app.config) # -1 top app1
app2 = create_app()
with app2.app_context(): # AppContext對象(app,g) -> local對象
print(current_app.config) # top -1 app2
print(current_app.config) # top -1 app1
# 寫離線腳本且多個上下文嵌套時,纔會在棧中添加多個對象。
# ---------------------------例2-----------------------------------
from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, {
'/backend': backend
})
# 使用 werkzeug 的 DispatcherMiddleware 實現多個 app 的分發,這種狀況下 _app_ctx_stack 棧裏會出現兩個 application context。