Flask上下文機制(Context)源碼分析

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都是取相應棧的棧頂元素,既然只取一個元素為什么要先入棧再出棧這么麻煩,直接保存這個對象不就行了嗎?
既然用了棧,說明這個棧里可能會有多個元素,舉例來說:

  1. 對于_request_ctx_stack可能保存了多個請求的上下文對象,因為有時候我們需要用到"internal redirect",就是說A請求進來了,在視圖函數里又發起了一個到B路由的請求,這樣當前線程中的Request Context就要同時維護B請求與A請求,并且是先處理完B再處理完A。
  2. 同樣的對于_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對象的生命周期顯然是不一樣的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容