Flask框架中有許多魔法將Web應用開發者與一些細節隔離開來,其中Context機制又是有別于其他框架的,這個機制讓開發人員在處理web請求時可以非常簡單的獲取上下文。為了理解Context機制,下載了Flask的0.10版本的源碼進行分析,雖然與最新的版本已經有了一點點區別,但是還是可以看出Flask作者最基本的設計思路。Context機制實現了應用上下文(App Context)、請求上下文(Request Context)、session、g這4個上下文。這4個上下文的作用以及使用方式就不介紹了,可以查看Flask官方文檔,本文主要通過分析源碼來介紹一些官方文檔中沒有說明的具體實現方式,以請求上下文為例,其他三個實現方式類似。
問題1:Request Context對象是如何保存的呢?
在寫Flask視圖代碼時,我們會通過from flask import request 來引入Request Context,request 對象來自于flask/globals.py。可以看到:
from werkzeug.local import LocalStack, LocalProxy
request = LocalProxy(partial(_lookup_req_object, 'request'))
也就是說request是一個LocalProxy實例。LocalProxy對象很顯然是一個代理類,那這個類代理的是什么呢?從from werkzeug.local import LocalProxy我們可以知道這個類來自于werkzeug,打開werkzeug.local可以看到:
class LocalProxy(object):
def __init__(self, local, name=None):
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__)
LocalProxy需要兩個初始化屬性('_LocalProxy__local' 以及 'name'),還定義了一個_get_current_object方法,這些就是這個類的關鍵了,'_LocalProxy__local' 是一個方法,通過調用這個方法可以獲取真實的對象,而'name' 這個屬性就是調用'_LocalProxy__local'所需的參數。_get_current_object負責真正去調用'_LocalProxy__local'方法(確切的說是可調用對象)并返回真實的對象。
回到request對象對照著來看一下,因為
request = LocalProxy(partial(_lookup_req_object, 'request'))
所以request的真實對象是通過_lookup_req_object方法以及'request'參數來獲取的。_lookup_req_object也定義在flask/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)
從上面的代碼可以看出request對象實際上是_request_ctx_stack對象的top屬性下的一個成員。那么_request_ctx_stack又是什么呢?同樣在flask/globals.py,我們可以看到:
_request_ctx_stack = LocalStack()
也就是說_request_ctx_stack是LocalStack類的實例,那么LocalStack又是什么呢?這個得再回到werkzeug.local,我們可以看到LocalStack類,代碼如下(為方便閱讀,已經過刪減):
class LocalStack(object):
def __init__(self):
self._local = Local()
def push(self, obj):
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
@property
def top(self):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
可以看到_request_ctx_stack的top屬性實際上是一個property裝飾的方法,獲取的是self._local.stack的最后一個元素,如果沒有就返回None,而self._local則是Local的實例,到這里Request Context維持的關鍵實現要出現了,Local的代碼如下(同樣已經過刪減):
class Local(object):
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
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}
可以看到Local的'__storage__'是一個dict,用key-value的形式來保存對象,key在__getattr__與__setattr__通過get_ident方法來獲取(這是一個關鍵的方法,問題3中再講),value同樣也是一個dict,這個dict的key-value則是調用者(LocalStack)給的,接下來只要原路返回去看LocalStack的代碼,可以發現LocalStack的push方法向Lock()的__storage__中的'stack'添加了一個obj對象,很顯然obj就是真正的request對象了,我們在使用flask.request時實際上使用的就是這個對象(但我們還不知道這個對象是什么,問題2中會介紹)。
上面說的太復雜了,總結一下來說就是:request是通過LocalProxy代理的_request_ctx_stack對象(LocalStack實例)的_local屬性(Local實例)中維持的一個字典內key為'stack'的值(一個數組)的棧頂的元素來保存。
再簡單一點來說就是request放在一個棧里,我們通過獲取棧頂元素來獲取當前的request。那么request是什么時候添加到這個棧中的呢,為什么這個棧的棧頂一定是當前的request呢?請看問題2
問題2:Request Context的生命周期是怎么樣的?
上面提到了LocalStack可以push Request Context到棧中, 那這個push方法很顯然是在請求到flask主進程時執行的,所以先看下Flask類(在flask/app.py中),代碼如下:
class Flask(_PackageBoundObject):
request_class = Request
...
def request_context(self, environ):
return RequestContext(self, environ)
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
error = None
try:
try:
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.make_response(self.handle_exception(e))
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
可以看到Flask.request_class是Request類,這個類在flask/wrappers.py中實現,繼承自werkzeug.wrappers.Request包含了請求的具體信息,這個就是request的真實對象了,但并不是Request Context,Request Context需要通過Flask.request_context方法獲取,這個方法很簡單返回了一個RequestContext實例,所以繼續看RequestContext類的代碼,如下(已經過刪減):
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.flashes = None
self.session = None
def push(self):
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)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_request_ctx_stack.push(self)
def pop(self):
....
def auto_pop(self, exc):
....
可以看到RequestContext中的request是通過app.request_class(environ)返回的,app就是上面Flask類的實例,所以app.request_class就是Flask.request_class,到這里Request Context 就算生成了,而且從上面的代碼中可以看到RequestContext中的push方法將Request Context push到了_request_ctx_stack中,這個push方法是在Flask.wsgi_app中調用的,所以問題到這里就清楚了。
總結一下Request Context的生命周期:Flask實例在獲得外部請求時,調用實例app的wsgi_app方法生成RequestContext對象并push到_request_ctx_stack中,通過LocalStack來維持,請求處理過程中通過LocalProxy來獲取,當請求處理完畢后調用RequestContext.auto_pop()刪除Request Context。
問題3:Request Context如何做到thread/greenlet隔離?
這個問題的另一種說法是為什么通過flask.request獲取始終是當前thread/greenlet
在問題1中提到了get_ident方法,這個方法是通過如下代碼獲取的:
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
可以看到get_ident優先從greenlet.getcurrent獲取,其次從thread獲取。get_ident()其實返回了greenlet/thread的id,所以在_request_ctx_stack中的獲取或者添加Request Context都是在這個id下面,所以就實現了隔離。
總結一下:通過LocalStack與Local的封裝以及get_ident方法,_request_ctx_stack.top就始終指向當前thread/greenlet的Request Context。
其他3個Context:
上面分析了Request Context的源碼,App Context的實現方式也是類似的,不同于Request Context,App Context維護在_app_ctx_stack中,但也是LocalStack的實例,代理實例則是current_app = LocalProxy(_find_app),_find_app與_lookup_req_object類似,具體代碼如下:
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
可以看出實現的方式是一樣的。
g與session則是分別依附在App Context與Request Context中的,g維護在_app_ctx_stack里,代理實現如下:
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return getattr(top, name)
g = LocalProxy(partial(_lookup_app_object, 'g'))
而session維護在_request_ctx_stack里,代理實現如下:
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
session = LocalProxy(partial(_lookup_req_object, 'session'))
與Request Context的實現方式一致。
可以看出無論是App Context、Request Context、g還是session都是每個greenlet/thread一份,請求處理完后pop。所以App Context以及g是不會在請求間共享的,不要被他們的名字迷惑,g并非"global"。
最后一個問題:為什么要用棧來維護這些Context?
上面的分析看下來,應該還有一個疑問:所有的Context都是取相應棧的棧頂元素,既然只取一個元素為什么要先入棧再出棧這么麻煩,直接保存這個對象不就行了嗎?
既然用了棧,說明這個棧里可能會有多個元素,舉例來說:
- 對于_request_ctx_stack可能保存了多個請求的上下文對象,因為有時候我們需要用到"internal redirect",就是說A請求進來了,在視圖函數里又發起了一個到B路由的請求,這樣當前線程中的Request Context就要同時維護B請求與A請求,并且是先處理完B再處理完A。
- 同樣的對于_app_ctx_stack,在有"internal redirect"時,App Context也需要是多份的。而棧這個數據結構的特點就是先進后出恰好符合了需求,所以要用棧。
最最后一個問題:為什么要有App Context?
Request Context以及session存在的目的是很顯然的,因為每個請求需要不同的請求上下文與session,不然就亂套了,那為什么App Context也是每個請求一份呢?App Context里維護的是一個配置以及路由等信息,這些信息是不會隨著請求變化的,難道App Context就是為了獲取g這個對象?但是在Flask 0.10之前的版本里g其實是放在Request Context里的,所以也不是為g,而是有其它原因。
其實App Context存在的目的是為了實現多應用,參考Flask官網代碼例子:
from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, { '/backend': backend})
上面的代碼就實現了在同一個WSGI服務中加載兩個Flask實例,所以請求的App Context并不是一定的,需要給每個請求都帶上一個App Context。
但是App Context與Request Context、sesson、g不一樣的地方在于App Context里最終都是一個Flask實例,對于同一個Flask實例來說對象是不變的,多個請求發生時只是引用計數的改變,對象始終還是那一個,但Request Context、sesson、g則是每個請求開始處理之前新建對象,請求處理完了再由垃圾回收機制來回收,因為被pop了以后就引用計數就是0了,其實很好理解,盡管App Context生命周期與Request Context一樣,但Flask實例與request對象的生命周期顯然是不一樣的。