Flask源碼之請求上下文和應用上下文(三)

原理

Request

Flask把前端傳過來的數(shù)據(jù)environ封裝成了 flask.wrappers.Request

這個類的實例又是RequestContextrequest屬性值

class RequestContext(object):
    request=flask.wrappers.Request(environ)

當然實際的代碼是這樣

class RequestContext(object):
    request=app.request_class(environ)

這個app.request_class= flask.wrappers.Request

之所以這樣寫印叁,是為了擴展性情屹,你可以修改Flaskrequest_class屬性來自定義你的Request

from flask import g, request
    
def get_tenant_from_request():
    auth = validate_auth(request.headers.get('Authorization'))
    return Tenant.query.get(auth.tenant_id)
        
def get_current_tenant():
    rv = getattr(g, 'current_tenant', None)
    if rv is = None:
        rv = get_tenant_from_request()
        g.current_tenant = rv
    return rv

例如

import flask
class MyFlask(flask.Flask):
    request_class=MyRequest
    
class MyRequest(flask.wrappers.Request):
    pass

繼續(xù)來看RequestContext,這個類在源碼中實例化了

ctx=RequestContext(self,envirion)

所以诗良,也就是說以后我們只要拿到這個實例ctx,然后訪問ctx.request就相當于訪問flask.wrappers.Request了舌狗,也就相當于可以訪問envirion,俱两,而RequestContext就是請求上下文饱狂。

ctx存儲在哪里呢?怎么訪問呢锋华?

RequestContext

存儲

實際上嗡官,ctx存在棧結構中,也就是后進先出毯焕,這是為了處理一個客戶端請求需要多個ctx的情況

用偽代碼表示就是

stack=[RequestContext(),RequestContext()]

而且衍腥,我們知道有的wsgi server對于每個請求都開一個線程磺樱,因此為了處理多線程隔離的情況,這個棧結構又存在了local中婆咸,這個local數(shù)據(jù)結構類似ThreadLocal竹捉,他們共同組成了LocalStack

用偽代碼表示就是

localstack={0:{"stack":stack}} # 0是線程id或者協(xié)程id

訪問

訪問ctx.request也不是直接訪問的,是通過一個代理類尚骄,叫LocalProxy块差,他是代理模式在flask中的應用

具體來說你要訪問 ctx.request 的某個屬性,先訪問LocalProxy的對應屬性倔丈,LocalProxy幫你訪問

LocalProxy代理了對 ctx.request的所有操作

偽代碼就是

# 例如flask.wrapper.Request()有一個get_json()方法
# localproxy也實現(xiàn)這個方法憨闰,幫我們訪問
class LocalProxy(object):
    def get_json(self):
        # 從localstack中獲取請求上下文
        ctx:RequestContext=local["這次請求的線程id"]["stack"].top
        json_data=ctx.request.get_json()
        return json_data

當然這個例子是不真實的,如果對于每個 flask.wrapper.Request的方法我們都在 LocalProxy實現(xiàn)一遍需五,那太麻煩了

request

我們經(jīng)常會引用這個request對象鹉动,它實際上就是LocalProxy的實例,位置在flask.globals.py

from flask import request
request = LocalProxy(partial(_lookup_req_object, "request"))

他代理了對 RequestContext.request也就是flask.wrappers.Request實例的操作

current_appg則分別代理了對AppContext.app(也就是flask實例)和 AppContext.g的操作

實現(xiàn)

Stack

一個棧結構宏邮,一般要實現(xiàn) push,top,pop這幾個方法

class Stack(object):
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def is_empty(self):
        return self.items == []

    def pop(self):
        if self.is_empty():
            return None
        # 后進先出
        return self.items[-1]
![image-20210123001932225](https://upload-images.jianshu.io/upload_images/9003674-0a0db7f8b46d9d24.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Local

local源碼泽示,作用就是線程或者協(xié)程隔離

class Local(object):
    __slots__ = ("__storage__", "__ident_func__")

    def __init__(self):
        # 實例化的時候給self綁定__storage__屬性,初始值是{}
        object.__setattr__(self, "__storage__", {})
        # 實例化的時候給self綁__ident_func__屬性蜜氨,初始值是get_ident函數(shù)械筛,這個方法用于獲取當前線程或協(xié)程id
        object.__setattr__(self, "__ident_func__", get_ident)
        
    def __setattr__(self, name, value):
        # 這個方法會在屬性被設置時調用
        # 因此如果我們這樣操作
        # s=Stack()
        # s.a=1
        # 那么self.__storage__屬性就變成了 {0:{"a",1}},0表示當前線程id
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}
            
    def __getattr__(self, name):
        # 獲取屬性時,該方法會被調用
        # 容易看出飒炎,通過把線程或協(xié)程id設置為key埋哟,可以實現(xiàn)線程或寫成隔離
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __iter__(self):
        # 迭代的時候會被調用
        return iter(self.__storage__.items())

    def __release_local__(self):
        # 清空當前線程的堆棧數(shù)據(jù)
        self.__storage__.pop(self.__ident_func__(), None)

    def __delattr__(self, name):
        # 刪除屬性時候會被調用
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

LocalStack

LocalStack大概相當于

{0:{"stack":[ctx,]}}
class LocalStack(object):

    def __init__(self):
        # _local屬性是Local對象,相當于字典{}
        # push方法就是給_local實例添加一個stack屬性厌丑,初始值是[]定欧,然后append
        # {0:'stack':[]}
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()

    @property
    def __ident_func__(self):
        return self._local.__ident_func__

    @__ident_func__.setter
    def __ident_func__(self, value):
        object.__setattr__(self._local, "__ident_func__", value)

    def push(self, obj):
        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
        
    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError("object unbound")
            return rv

        return LocalProxy(_lookup)

值得注意的是,當我們執(zhí)行如下代碼

ls=LocalStack()
ls()

會調用 __call__方法怒竿,返回的是目前棧頂對象的代理對象,棧頂對象例如RequestContext

LocalProxy

LocalProxy就是代理對象了扩氢,它可以代理對RequestContext的操作

class LocalProxy(object):
    __slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
    def __init__(self, local, name=None):     
        object.__setattr__(self, "_LocalProxy__local", local)


    def _get_current_object(self):
        if not hasattr(self.__local, "__release_local__"):
            return self.__local()

    def __getattr__(self, name):
        return getattr(self._get_current_object(), name)

原理很簡單耕驰,我們訪問 LocalProxy的某個屬性,會調用 __getattr__方法录豺,__getattr__方法又會調用 _get_current_object去獲取棧頂對象或者棧頂對象的屬性朦肘,例如獲取RequestContext對象或者RequestContext.request

request對象為例

他是一個LocalProxy的實例

request = LocalProxy(partial(_lookup_req_object, "request"))

LocalProxy實例化傳入了一個偏函數(shù) _lookup_req_object(偏函數(shù)作用就是固定函數(shù)的參數(shù)),這個函數(shù)的作用就是獲取棧頂?shù)?code>RequestContext對象的request屬性双饥,也就是 flask.wrappers.Request()

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

也就是說 LocalProxy__local就是 partial(_lookup_req_object, "request")

那么 _get_current_object實際上就是執(zhí)行 partial(_lookup_req_object, "request")()來獲取棧頂對象

入棧

我們繼續(xù)來看wsgi_app這個方法媒抠,我去掉了一些無關代碼,那些部分會在其他博文中介紹

def wsgi_app(self, environ, start_response):
    # 調用了self.request_context這個方法
    # 此方法把environ封裝成了RequestContext對象
    ctx = self.request_context(environ)      
def request_context(self, environ):
    # 注意request_context是Flask的類方法咏花,那么self就是Flask的實例或者說對象
    # 也就是說下面的 RequestContext 的實例化需要Flask實例和environ參數(shù)
    return RequestContext(self, environ)

簡單看一下RequestContext的定義趴生,位置在源碼的ctx.py

class RequestContext(object):
    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request

回到wsgi_app這個方法阀趴,我們看到接下來ctx.push這一句調用了RequestContextpush方法,這個方法就是把自身也就是 RequestContext實例壓入到 LocalStack這個數(shù)據(jù)結構中

    def wsgi_app(self, environ, start_response):
        # ctx是 RequestContext 實例
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                # 只看這一句
                ctx.push()
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

push方法

    def push(self):
        top = _request_ctx_stack.top

_request_ctx_stack的定義在globals.py中苍匆,就是創(chuàng)建了一個空的本地棧

_request_ctx_stack = LocalStack()

它現(xiàn)在的狀態(tài)是這樣刘急,棧是空的

{0:'stack':[]}

如果棧是空的話,我們把self也就是RequestContext壓入棧(最后一句)浸踩,注意pushRequestContext的方法叔汁,所以self就是RequestContext的實例

    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)

我們還注意到這里還有一個_app_ctx_stack,這也是LocalStack检碗,位置在globals.py据块,他現(xiàn)在也是空的

_app_ctx_stack = LocalStack()

只不過這個棧里面存儲的是應用上下文,類似下面的字典

{0:'stack':[AppContext]}

然后我們也執(zhí)行了app_ctx.push()方法折剃,也就是把應用上下文壓入棧

到這里我們就知道了另假,執(zhí)行RequestContext對象的push方法會把RequestContext的實例壓入_request_ctx_stack中,還會把AppContext的實例壓入_app_ctx_stack

為什么要用LocalProxy

我們常常需要在一個視圖函數(shù)中獲取客戶端請求中的參數(shù)微驶,例如url浪谴,remote_address

我們當然可以每次手動獲取_request_ctx_stack棧頂?shù)?code>RequestContext對象,然后調用RequestContextrequest屬性因苹,但每次操作棧結構還是有點繁瑣苟耻,像下面這樣

from flask import Flask, request, Request
from flask.globals import _request_ctx_stack

flask_app = Flask(__name__)


@flask_app.route('/')
def hello_world():
    req: Request = _request_ctx_stack.top.request
    remote_addr=req.remote_addr
    return "{}".format(remote_addr)

flask的做法是使用LocalProxy

from flask import Flask, request

我們執(zhí)行request.remote_addr就相當于執(zhí)行 _request_ctx_stack.top.request.remote_addr

那為什么用LocalProxy而不是直接request=_request_ctx_stack.top.request呢?

原因是這樣寫扶檐,項目run的時候凶杖,這句 request=_request_ctx_stack.top.request 就已經(jīng)執(zhí)行了(因為被引用了),但是項目啟動的時候_request_ctx_stack.top還是None款筑,因為還沒有請求進來智蝠,push方法還沒執(zhí)行。這就導致了request固定成了None奈梳,這顯然不行

LocalProxy 重寫了__getattr__方法杈湾,讓每次執(zhí)行 request.remote_addr會先去 LocalStack中拿到 RequestContext,然后執(zhí)行 RequestContext.request.remote_addr,獲取其他屬性也是一樣

也就是說攘须,代理模式延遲了 被代理對象的獲取漆撞,代理對象Localproxy創(chuàng)建的時候不會獲取,獲取被代理對象屬性的時候才會獲取被代理對象

總結

到這里于宙,我們就可以看出flask中的請求上下文是如何存儲和獲取的

請求上下文存儲在LocalStack結構中浮驳,Local是為了線程安全,LocalStack是為了多請求上下文的場景

而獲取是通過LocalProxy捞魁,使用代理模式是為了動態(tài)地獲取請求上下文至会,在訪問request屬性的時候,才會從棧中獲取真實的請求上下文谱俭,然后代理 屬性的獲取

flask中還有應用上下文奉件,current_app宵蛀,我們有時候會通過current_app.config來獲取配置信息,原理和request類似瓶蚂,只不過代理的是AppContext對象

說了這么多糖埋,wsgi_app的第一步,也就是請求的第一步窃这,就是先把請求上下文和應用上下文入棧

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末瞳别,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子杭攻,更是在濱河造成了極大的恐慌祟敛,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兆解,死亡現(xiàn)場離奇詭異馆铁,居然都是意外死亡,警方通過查閱死者的電腦和手機锅睛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門埠巨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人现拒,你說我怎么就攤上這事辣垒。” “怎么了印蔬?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵勋桶,是天一觀的道長。 經(jīng)常有香客問我侥猬,道長例驹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任退唠,我火速辦了婚禮鹃锈,結果婚禮上,老公的妹妹穿的比我還像新娘瞧预。我一直安慰自己仪召,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布松蒜。 她就那樣靜靜地躺著,像睡著了一般已旧。 火紅的嫁衣襯著肌膚如雪秸苗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天运褪,我揣著相機與錄音惊楼,去河邊找鬼玖瘸。 笑死,一個胖子當著我的面吹牛檀咙,可吹牛的內容都是我干的雅倒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼弧可,長吁一口氣:“原來是場噩夢啊……” “哼蔑匣!你這毒婦竟也來了?” 一聲冷哼從身側響起棕诵,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤裁良,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后校套,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體价脾,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年笛匙,在試婚紗的時候發(fā)現(xiàn)自己被綠了侨把。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡妹孙,死狀恐怖秋柄,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情涕蜂,我是刑警寧澤华匾,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站机隙,受9級特大地震影響蜘拉,放射性物質發(fā)生泄漏。R本人自食惡果不足惜有鹿,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一旭旭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧葱跋,春花似錦持寄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荠卷,卻和暖如春模庐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背油宜。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工掂碱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留怜姿,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓疼燥,卻偏偏與公主長得像沧卢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子醉者,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內容