flask 源碼解析:上下文

4.flask 源碼解析:上下文

上下文(application context 和 request context)

上下文一直是計(jì)算機(jī)中難理解的概念,在知乎的一個(gè)問(wèn)題下面有個(gè)很通俗易懂的回答:

每一段程序都有很多外部變量。只有像Add這種簡(jiǎn)單的函數(shù)才是沒(méi)有外部變量的。一旦你的一段程序有了外部變量,這段程序就不完整,不能獨(dú)立運(yùn)行。你為了使他們運(yùn)行,就要給所有的外部變量一個(gè)一個(gè)寫一些值進(jìn)去。這些值的集合就叫上下文。
– vzch

比如,在 flask 中,視圖函數(shù)需要知道它執(zhí)行情況的請(qǐng)求信息(請(qǐng)求的 url,參數(shù),方法等)以及應(yīng)用信息(應(yīng)用中初始化的數(shù)據(jù)庫(kù)等),才能夠正確運(yùn)行。

最直觀地做法是把這些信息封裝成一個(gè)對(duì)象,作為參數(shù)傳遞給視圖函數(shù)。但是這樣的話,所有的視圖函數(shù)都需要添加對(duì)應(yīng)的參數(shù),即使該函數(shù)內(nèi)部并沒(méi)有使用到它。

flask 的做法是把這些信息作為類似全局變量的東西,視圖函數(shù)需要的時(shí)候,可以使用 from flask import request 獲取。但是這些對(duì)象和全局變量不同的是——它們必須是動(dòng)態(tài)的,因?yàn)樵诙嗑€程或者多協(xié)程的情況下,每個(gè)線程或者協(xié)程獲取的都是自己獨(dú)特的對(duì)象,不會(huì)互相干擾。

那么如何實(shí)現(xiàn)這種效果呢?如果對(duì) python 多線程比較熟悉的話,應(yīng)該知道多線程中有個(gè)非常類似的概念 threading.local,可以實(shí)現(xiàn)多線程訪問(wèn)某個(gè)變量的時(shí)候只看到自己的數(shù)據(jù)。內(nèi)部的原理說(shuō)起來(lái)也很簡(jiǎn)單,這個(gè)對(duì)象有一個(gè)字典,保存了線程 id 對(duì)應(yīng)的數(shù)據(jù),讀取該對(duì)象的時(shí)候,它動(dòng)態(tài)地查詢當(dāng)前線程 id 對(duì)應(yīng)的數(shù)據(jù)。flaskpython 上下文的實(shí)現(xiàn)也類似,后面會(huì)詳細(xì)解釋。

flask 中有兩種上下文:application contextrequest context。上下文有關(guān)的內(nèi)容定義在 globals.py 文件,文件的內(nèi)容也非常短:

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


# context locals
_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'))

flask 提供兩種上下文:application contextrequest context 。app lication context 又演化出來(lái)兩個(gè)變量 current_appg,而 request context 則演化出來(lái) requestsession。

這里的實(shí)現(xiàn)用到了兩個(gè)東西:LocalStackLocalProxy。它們兩個(gè)的結(jié)果就是我們可以動(dòng)態(tài)地獲取兩個(gè)上下文的內(nèi)容,在并發(fā)程序中每個(gè)視圖函數(shù)都會(huì)看到屬于自己的上下文,而不會(huì)出現(xiàn)混亂。

LocalStackLocalProxy 都是 werkzeug 提供的,定義在 local.py 文件中。在分析這兩個(gè)類之前,我們先介紹這個(gè)文件另外一個(gè)基礎(chǔ)的類 Local。Local 就是實(shí)現(xiàn)了類似 threading.local 的效果——多線程或者多協(xié)程情況下全局變量的隔離效果。下面是它的代碼:

# since each thread has its own greenlet we can just use those as identifiers
# for the context.  If greenlets are not available we fall back to the
# current thread ident depending on where it is.
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):
        # 數(shù)據(jù)保存在 __storage__ 中,后續(xù)訪問(wèn)都是對(duì)該屬性的操作
        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)

    # 清空當(dāng)前線程/協(xié)程保存的所有數(shù)據(jù)
    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    # 下面三個(gè)方法實(shí)現(xiàn)了屬性的訪問(wèn)、設(shè)置和刪除。
    # 注意到,內(nèi)部都調(diào)用 `self.__ident_func__` 獲取當(dāng)前線程或者協(xié)程的 id,然后再訪問(wèn)對(duì)應(yīng)的內(nèi)部字典。
    # 如果訪問(wèn)或者刪除的屬性不存在,會(huì)拋出 AttributeError。
    # 這樣,外部用戶看到的就是它在訪問(wèn)實(shí)例的屬性,完全不知道字典或者多線程/協(xié)程切換的實(shí)現(xiàn)
    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 對(duì)象內(nèi)部的數(shù)據(jù)都是保存在 __storage__ 屬性的,這個(gè)屬性變量是個(gè)嵌套的字典:map[ident]map[key]value。最外面字典 key 是線程或者協(xié)程的 identity,value 是另外一個(gè)字典,這個(gè)內(nèi)部字典就是用戶自定義的 key-value 鍵值對(duì)。用戶訪問(wèn)實(shí)例的屬性,就變成了訪問(wèn)內(nèi)部的字典,外面字典的 key 是自動(dòng)關(guān)聯(lián)的。__ident_func 是 協(xié)程的 get_current 或者線程的 get_ident,從而獲取當(dāng)前代碼所在線程或者協(xié)程的 id。

除了這些基本操作之外,Local 還實(shí)現(xiàn)了 __release_local__ ,用來(lái)清空(析構(gòu))當(dāng)前線程或者協(xié)程的數(shù)據(jù)(狀態(tài))。__call__ 操作來(lái)創(chuàng)建一個(gè) LocalProxy 對(duì)象,LocalProxy 會(huì)在下面講到。

理解了 Local,我們繼續(xù)回來(lái)看另外兩個(gè)類。

LocalStack 是基于 Local 實(shí)現(xiàn)的棧結(jié)構(gòu)。如果說(shuō) Local 提供了多線程或者多協(xié)程隔離的屬性訪問(wèn),那么 LocalStack 就提供了隔離的棧訪問(wèn)。下面是它的實(shí)現(xiàn)代碼,可以看到它提供了 push、poptop 方法。

__release_local__ 可以用來(lái)清空當(dāng)前線程或者協(xié)程的棧數(shù)據(jù),__call__ 方法返回當(dāng)前線程或者協(xié)程棧頂元素的代理對(duì)象。

class LocalStack(object):
    """This class works similar to a :class:`Local` but keeps a stack
    of objects instead. """

    def __init__(self):
        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 三個(gè)方法實(shí)現(xiàn)了棧的操作,
    # 可以看到棧的數(shù)據(jù)是保存在 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):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        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):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

我們?cè)谥翱吹搅?request context 的定義,它就是一個(gè) LocalStack 的實(shí)例:

_request_ctx_stack = LocalStack()

它會(huì)當(dāng)前線程或者協(xié)程的請(qǐng)求都保存在棧里,等使用的時(shí)候再?gòu)睦锩孀x取。至于為什么要用到棧結(jié)構(gòu),而不是直接使用 Local,我們會(huì)在后面揭曉答案,你可以先思考一下。

LocalProxy 是一個(gè) Local 對(duì)象的代理,負(fù)責(zé)把所有對(duì)自己的操作轉(zhuǎn)發(fā)給內(nèi)部的 Local 對(duì)象。LocalProxy 的構(gòu)造函數(shù)介紹一個(gè) callable 的參數(shù),這個(gè) callable 調(diào)用之后需要返回一個(gè) Local 實(shí)例,后續(xù)所有的屬性操作都會(huì)轉(zhuǎn)發(fā)給 callable 返回的對(duì)象。

class LocalProxy(object):
    """Acts as a proxy for a werkzeug local.
    Forwards all operations to a proxied object. """
    __slots__ = ('__local', '__dict__', '__name__')

    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)

    def _get_current_object(self):
        """Return the current object."""
        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

這里實(shí)現(xiàn)的關(guān)鍵是把通過(guò)參數(shù)傳遞進(jìn)來(lái)的 Local 實(shí)例保存在 __local 屬性中,并定義了 _get_current_object() 方法獲取當(dāng)前線程或者協(xié)程對(duì)應(yīng)的對(duì)象。

NOTE:前面雙下劃線的屬性,會(huì)保存到 _ClassName__variable 中。所以這里通過(guò) “_LocalProxy__local” 設(shè)置的值,后面可以通過(guò) self.__local 來(lái)獲取。關(guān)于這個(gè)知識(shí)點(diǎn),可以查看 stackoverflow 的這個(gè)問(wèn)題

然后 LocalProxy 重寫了所有的魔術(shù)方法(名字前后有兩個(gè)下劃線的方法),具體操作都是轉(zhuǎn)發(fā)給代理對(duì)象的。這里只給出了幾個(gè)魔術(shù)方法,感興趣的可以查看源碼中所有的魔術(shù)方法。

繼續(xù)回到 request context 的實(shí)現(xiàn):

_request_ctx_stack = LocalStack()
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))

再次看這段代碼希望能看明白,_request_ctx_stack 是多線程或者協(xié)程隔離的棧結(jié)構(gòu),request 每次都會(huì)調(diào)用 _lookup_req_object 棧頭部的數(shù)據(jù)來(lái)獲取保存在里面的 requst context。

那么請(qǐng)求上下文信息是什么被放在 stack 中呢?還記得之前介紹的 wsgi_app() 方法有下面兩行代碼嗎?

ctx = self.request_context(environ)
ctx.push()

每次在調(diào)用 app.__call__ 的時(shí)候,都會(huì)把對(duì)應(yīng)的請(qǐng)求信息壓棧,最后執(zhí)行完請(qǐng)求的處理之后把它出棧。

我們來(lái)看看request_context, 這個(gè) 方法只有一行代碼:

def request_context(self, environ):
    return RequestContext(self, environ)

它調(diào)用了 RequestContext,并把 self 和請(qǐng)求信息的字典 environ 當(dāng)做參數(shù)傳遞進(jìn)去。追蹤到 RequestContext 定義的地方,它出現(xiàn)在 ctx.py 文件中,代碼如下:

class RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.
    """

    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):
        """Can be overridden by a subclass to hook into the matching
        of the request.
        """
        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):
        """Binds the request context to the current context."""
        # Before we push the request context we have to ensure that there
        # is an application context.
        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):
        """Pops the request context and unbinds it by doing that.  This will
        also trigger the execution of functions registered by the
        :meth:`~flask.Flask.teardown_request` decorator.
        """
        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()

            # get rid of circular dependencies at the end of the request
            # so that we don't require the GC to be active.
            if clear_request:
                rv.request.environ['werkzeug.request'] = None

            # Get rid of the app as well if necessary.
            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)

每個(gè) request context 都保存了當(dāng)前請(qǐng)求的信息,比如 request 對(duì)象和 app 對(duì)象。在初始化的最后,還調(diào)用了 match_request 實(shí)現(xiàn)了路由的匹配邏輯。

push 操作就是把該請(qǐng)求的 ApplicationContext(如果 _app_ctx_stack 棧頂不是當(dāng)前請(qǐng)求所在 app ,需要?jiǎng)?chuàng)建新的 app context) 和 RequestContext 有關(guān)的信息保存到對(duì)應(yīng)的棧上,壓棧后還會(huì)保存 session 的信息; pop 則相反,把 request context 和 application context 出棧,做一些清理性的工作。

到這里,上下文的實(shí)現(xiàn)就比較清晰了:每次有請(qǐng)求過(guò)來(lái)的時(shí)候,flask 會(huì)先創(chuàng)建當(dāng)前線程或者進(jìn)程需要處理的兩個(gè)重要上下文對(duì)象,把它們保存到隔離的棧里面,這樣視圖函數(shù)進(jìn)行處理的時(shí)候就能直接從棧上獲取這些信息。

NOTE:因?yàn)?app 實(shí)例只有一個(gè),因此多個(gè) request 共享了 application context

到這里,關(guān)于 context 的實(shí)現(xiàn)和功能已經(jīng)講解得差不多了。還有兩個(gè)疑惑沒(méi)有解答。

  1. 為什么要把 request context 和 application context 分開(kāi)?每個(gè)請(qǐng)求不是都同時(shí)擁有這兩個(gè)上下文信息嗎?
  2. 為什么 request context 和 application context 都有實(shí)現(xiàn)成棧的結(jié)構(gòu)?每個(gè)請(qǐng)求難道會(huì)出現(xiàn)多個(gè) request context 或者 application context 嗎?

第一個(gè)答案是“靈活度”,第二個(gè)答案是“多 application”。雖然在實(shí)際運(yùn)行中,每個(gè)請(qǐng)求對(duì)應(yīng)一個(gè) request context 和一個(gè) application context,但是在測(cè)試或者 python shell 中運(yùn)行的時(shí)候,用戶可以單獨(dú)創(chuàng)建 request context 或者 application context,這種靈活度方便用戶的不同的使用場(chǎng)景;而且棧可以讓 redirect 更容易實(shí)現(xiàn),一個(gè)處理函數(shù)可以從棧中獲取重定向路徑的多個(gè)請(qǐng)求信息。application 設(shè)計(jì)成棧也是類似,測(cè)試的時(shí)候可以添加多個(gè)上下文,另外一個(gè)原因是 flask 可以多個(gè) application 同時(shí)運(yùn)行:

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend

application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

這個(gè)例子就是使用 werkzeugDispatcherMiddleware 實(shí)現(xiàn)多個(gè) app 的分發(fā),這種情況下 _app_ctx_stack 棧里會(huì)出現(xiàn)兩個(gè) application context。

Update: 為什么要用 LocalProxy

寫完這篇文章之后,收到有位讀者的疑問(wèn):為什么要使用 LocalProxy?不使用 LocalProxy 直接訪問(wèn) LocalStack 的對(duì)象會(huì)有什么問(wèn)題嗎?

這是個(gè)很好的問(wèn)題,上面也確實(shí)沒(méi)有很明確地給出這個(gè)答案。這里解釋一下!

首先明確一點(diǎn),LocalLocalStack 實(shí)現(xiàn)了不同線程/協(xié)程之間的數(shù)據(jù)隔離。在為什么用 LocalStack 而不是直接使用 Local 的時(shí)候,我們說(shuō)過(guò)這是因?yàn)?flask 希望在測(cè)試或者開(kāi)發(fā)的時(shí)候,允許多 app 、多 request 的情況。而 LocalProxy 也是因?yàn)檫@個(gè)才引入進(jìn)來(lái)的!

我們拿 current_app = LocalProxy(_find_app) 來(lái)舉例子。每次使用 current_app 的時(shí)候,他都會(huì)調(diào)用 _find_app 函數(shù),然后對(duì)得到的變量進(jìn)行操作。

如果直接使用 current_app = _find_app() 有什么區(qū)別呢?區(qū)別就在于,我們導(dǎo)入進(jìn)來(lái)之后,current_app 就不會(huì)再變化了。如果有多 app 的情況,就會(huì)出現(xiàn)錯(cuò)誤,比如:

from flask import current_app

app = create_app()
admin_app = create_admin_app()

def do_something():
    with app.app_context():
        work_on(current_app)
        with admin_app.app_context():
            work_on(current_app)

這里我們出現(xiàn)了嵌套的 app,每個(gè) with 上下文都需要操作其對(duì)應(yīng)的 app,如果不適用 LocalProxy 是做不到的。

對(duì)于 request 也是類似!但是這種情況真的很少發(fā)生,有必要費(fèi)這么大的功夫增加這么多復(fù)雜度嗎?

其實(shí)還有一個(gè)更大的問(wèn)題,這個(gè)例子也可以看出來(lái)。比如我們知道 current_app 是動(dòng)態(tài)的,因?yàn)樗澈髮?duì)應(yīng)的棧會(huì) push 和 pop 元素進(jìn)去。那剛開(kāi)始的時(shí)候,棧一定是空的,只有在 with app.app_context() 這句的時(shí)候,才把棧數(shù)據(jù) push 進(jìn)去。而如果不采用 LocalProxy 進(jìn)行轉(zhuǎn)發(fā),那么在最上面導(dǎo)入 from flask import current_app 的時(shí)候,current_app 就是空的,因?yàn)檫@個(gè)時(shí)候還沒(méi)有把數(shù)據(jù) push 進(jìn)去,后面調(diào)用的時(shí)候根本無(wú)法使用。

所以為什么需要 LocalProxy 呢?簡(jiǎn)單總結(jié)一句話:因?yàn)樯舷挛谋4娴臄?shù)據(jù)是保存在棧里的,并且會(huì)動(dòng)態(tài)發(fā)生變化。如果不是動(dòng)態(tài)地去訪問(wèn),會(huì)造成數(shù)據(jù)訪問(wèn)異常。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 上下文這個(gè)概念多見(jiàn)于文章中,是一句話中的語(yǔ)境,也就是語(yǔ)言環(huán)境。一句莫名其妙的話出現(xiàn)會(huì)讓人不理解什么意思,如果有語(yǔ)言...
    饅頭白啊白閱讀 31,741評(píng)論 6 66
  • 原文:Flask的Context(上下文)學(xué)習(xí)筆記作者:饅頭白啊白 上下文這個(gè)概念多見(jiàn)于文章中,是一句話中的語(yǔ)境,...
    氨基鈉閱讀 907評(píng)論 0 2
  • 庭院深深深幾許,楊柳堆煙,簾幕無(wú)重?cái)?shù)。玉勒雕鞍游冶處,樓高不見(jiàn)章臺(tái)路。雨橫風(fēng)狂三月暮,門掩黃昏,無(wú)計(jì)留春住。淚眼問(wèn)...
    SlashBoyMr_wang閱讀 3,433評(píng)論 2 4
  • [TOC]一直想做源碼閱讀這件事,總感覺(jué)難度太高時(shí)間太少,可望不可見(jiàn)。最近正好時(shí)間充裕,決定試試做一下,并記錄一下...
    何柯君閱讀 7,400評(píng)論 3 98
  • 西成高鐵(西安到成都)開(kāi)通一段時(shí)間了,卻總是時(shí)機(jī)再赴西南而錯(cuò)過(guò)體驗(yàn)。終于逮了個(gè)機(jī)會(huì)體驗(yàn)了一把在山洞中長(zhǎng)途穿梭的感覺(jué)...
    張永勝_永往直前閱讀 532評(píng)論 0 0

友情鏈接更多精彩內(nèi)容