Flask中的請求上下文和應用上下文

在Flask中處理請求時,應用會生成一個「請求上下文」對象。整個請求的處理過程,都會在這個上下文對象中進行。這保證了請求的處理過程不被幹擾。處理請求的具體代碼以下:python

def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
        # with語句中生成一個`response`對象
        ...
    return response(environ, start_response)

在Flask 0.9版本以前,應用只有「請求上下文」對象,它包含了和請求處理相關的信息。同時Flask還根據werkzeug.local模塊中實現的一種數據結構LocalStack用來存儲「請求上下文」對象。這在{% post_link 一個Flask應用運行過程剖析 一個Flask應用運行過程剖析 %}中有所介紹。在0.9版本中,Flask又引入了「應用上下文」的概念。本文主要Flask中的這兩個「上下文」對象。shell

LocalStack

在介紹「請求上下文」和「應用上下文」以前,咱們對LocalStack簡要作一個回顧。在Werkzeug庫——local模塊一文中,咱們講解了werkzeug.local模塊中實現的三個類LocalLocalStackLocalProxy。關於它們的概念和詳細介紹,能夠查看上面的文章。這裏,咱們用一個例子來講明Flask中使用的一種數據結構LocalStackflask

>>> from werkzeug.local import LocalStack
>>> import threading

# 建立一個`LocalStack`對象
>>> local_stack = LocalStack()
# 查看local_stack中存儲的信息
>>> local_stack._local.__storage__
{}

# 定義一個函數,這個函數能夠向`LocalStack`中添加數據
>>> def worker(i):
        local_stack.push(i)

# 使用3個線程運行函數`worker`
>>> for i in range(3):
        t = threading.Thread(target=worker, args=(i,))
        t.start()

# 再次查看local_stack中存儲的信息
>>> local_stack._local.__storage__
{<greenlet.greenlet at 0x4bee5a0>: {'stack': [2]},
 <greenlet.greenlet at 0x4bee638>: {'stack': [1]},
 <greenlet.greenlet at 0x4bee6d0>: {'stack': [0]}
}

由上面的例子能夠看出,存儲在LocalStack中的信息以字典的形式存在:鍵爲線程/協程的標識數值,值也是字典形式。每當有一個線程/協程上要將一個對象pushLocalStack棧中,會造成如上一個「鍵-值」對。這樣的一種結構很好地實現了線程/協程的隔離,每一個線程/協程都會根據本身線程/協程的標識數值肯定存儲在棧結構中的值。segmentfault

LocalStack還實現了pushpoptop等方法。其中top方法永遠指向棧頂的元素。棧頂的元素是指當前線程/協程中最後被推入棧中的元素,即local_stack._local.stack[-1](注意,是stack鍵對應的對象中最後被推入的元素)。session

請求上下文

Flask中全部的請求處理都在「請求上下文」中進行,在它設計之初便就有這個概念。因爲0.9版本代碼比較複雜,這裏仍是以0.1版本的代碼爲例進行說明。本質上這兩個版本的「請求上下文」的運行原理沒有變化,只是新版本增長了一些功能,這點在後面再進行解釋。數據結構

請求上下文——0.1版本

# Flask v0.1
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):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None

    def __enter__(self):
        _request_ctx_stack.push(self)

    def __exit__(self, exc_type, exc_value, tb):
        # do not pop the request stack if we are in debug mode and an
        # exception happened.  This will allow the debugger to still
        # access the request object in the interactive shell.
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

由上面「請求上下文」的實現可知:app

  • 「請求上下文」是一個上下文對象,實現了__enter____exit__方法。可使用with語句構造一個上下文環境。ide

  • 進入上下文環境時,_request_ctx_stack這個棧中會推入一個_RequestContext對象。這個棧結構就是上面講的LocalStack棧。函數

  • 推入棧中的_RequestContext對象有一些屬性,包含了請求的的全部相關信息。例如apprequestsessiongflashes。還有一個url_adapter,這個對象能夠進行URL匹配。post

  • with語句構造的上下文環境中能夠進行請求處理。當退出上下文環境時,_request_ctx_stack這個棧會銷燬剛纔存儲的上下文對象。

以上的運行邏輯使得請求的處理始終在一個上下文環境中,這保證了請求處理過程不被幹擾,並且請求上下文對象保存在LocalStack棧中,也很好地實現了線程/協程的隔離。

如下是一個簡單的例子:

# example - Flask v0.1
>>> from flask import Flask, _request_ctx_stack
>>> import threading
>>> app = Flask(__name__)
# 先觀察_request_ctx_stack中包含的信息
>>> _request_ctx_stack._local.__storage__
{}

# 建立一個函數,用於向棧中推入請求上下文
# 本例中不使用`with`語句
>>> def worker():
        # 使用應用的test_request_context()方法建立請求上下文
        request_context = app.test_request_context()
        _request_ctx_stack.push(request_context)

# 建立3個進程分別執行worker方法
>>> for i in range(3):
        t = threading.Thread(target=worker)
        t.start()

# 再觀察_request_ctx_stack中包含的信息
>>> _request_ctx_stack._local.__storage__
{<greenlet.greenlet at 0x5e45df0>: {'stack': [<flask._RequestContext at 0x710c668>]},
 <greenlet.greenlet at 0x5e45e88>: {'stack': [<flask._RequestContext at 0x7107f28>]},
 <greenlet.greenlet at 0x5e45f20>: {'stack': [<flask._RequestContext at 0x71077f0>]}
}

上面的結果顯示:_request_ctx_stack中爲每個線程建立了一個「鍵-值」對,每一「鍵-值」對中包含一個請求上下文對象。若是使用with語句,在離開上下文環境時棧中銷燬存儲的上下文對象信息。

請求上下文——0.9版本

在0.9版本中,Flask引入了「應用上下文」的概念,這對「請求上下文」的實現有必定的改變。這個版本的「請求上下文」也是一個上下文對象。在使用with語句進入上下文環境後,_request_ctx_stack會存儲這個上下文對象。不過與0.1版本相比,有如下幾點改變:

  • 請求上下文實現了pushpop方法,這使得對於請求上下文的操做更加的靈活;

  • 伴隨着請求上下文對象的生成並存儲在棧結構中,Flask還會生成一個「應用上下文」對象,並且「應用上下文」對象也會存儲在另外一個棧結構中去。這是兩個版本最大的不一樣。

咱們先看一下0.9版本相關的代碼:

# Flask v0.9
def push(self):
    """Binds the request context to the current context."""
    top = _request_ctx_stack.top
    if top is not None and top.preserved:
        top.pop()

    # 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()

咱們注意到,0.9版本的「請求上下文」的pop方法中,當要將一個「請求上下文」推入_request_ctx_stack棧中的時候,會先檢查另外一個棧_app_ctx_stack的棧頂是否存在「應用上下文」對象或者棧頂的「應用上下文」對象的應用是不是當前應用。若是不存在或者不是當前對象,Flask會自動先生成一個「應用上下文」對象,並將其推入_app_ctx_stack中。

咱們再看離開上下文時的相關代碼:

# Flask v0.9
def pop(self, exc=None):
    """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.

    .. versionchanged:: 0.9
       Added the `exc` argument.
    """
    app_ctx = self._implicit_app_ctx_stack.pop()

    clear_request = False
    if not self._implicit_app_ctx_stack:
        self.preserved = False
        if exc is None:
            exc = sys.exc_info()[1]
        self.app.do_teardown_request(exc)
        clear_request = True

    rv = _request_ctx_stack.pop()
    assert rv is self, 'Popped wrong request context.  (%r instead of %r)' \
        % (rv, self)

    # 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)

上面代碼中的細節先不討論。注意到當要離開以上「請求上下文」環境的時候,Flask會先將「請求上下文」對象從_request_ctx_stack棧中銷燬,以後會根據實際的狀況肯定銷燬「應用上下文」對象。

如下仍是以一個簡單的例子進行說明:



# example - Flask v0.9
# example - Flask v0.9
>>> from flask import Flask, _request_ctx_stack, _app_ctx_stack
>>> app = Flask(__name__)

# 先檢查兩個棧的內容
>>> _request_ctx_stack._local.__storage__
{}
>>> _app_ctx_stack._local.__storage__
{}

# 生成一個請求上下文對象
>>> request_context = app.test_request_context()
>>> request_context.push()

# 請求上下文推入棧後,再次查看兩個棧的內容
>>> _request_ctx_stack._local.__storage__
{<greenlet.greenlet at 0x6eb32a8>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
>>> _app_ctx_stack._local.__storage__
{<greenlet.greenlet at 0x6eb32a8>: {'stack': [<flask.ctx.AppContext at 0x5c96a58>]}}

>>> request_context.pop()

# 銷燬請求上下文時,再次查看兩個棧的內容
>>> _request_ctx_stack._local.__storage__
{}
>>> _app_ctx_stack._local.__storage__
{}

應用上下文

上部分中簡單介紹了「應用上下文」和「請求上下文」的關係。那什麼是「應用上下文」呢?咱們先看一下它的類:

class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0

    def push(self):
        """Binds the app context to the current context."""
        self._refcnt += 1
        _app_ctx_stack.push(self)

    def pop(self, exc=None):
        """Pops the app context."""
        self._refcnt -= 1
        if self._refcnt <= 0:
            if exc is None:
                exc = sys.exc_info()[1]
            self.app.do_teardown_appcontext(exc)
        rv = _app_ctx_stack.pop()
        assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \
            % (rv, self)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

由以上代碼能夠看出:「應用上下文」也是一個上下文對象,可使用with語句構造一個上下文環境,它也實現了pushpop等方法。「應用上下文」的構造函數也和「請求上下文」相似,都有appurl_adapter等屬性。「應用上下文」存在的一個主要功能就是肯定請求所在的應用。

然而,以上的論述卻又讓人產生這樣的疑問:既然「請求上下文」中也包含app等和當前應用相關的信息,那麼只要調用_request_ctx_stack.top.app或者魔法current_app就能夠肯定請求所在的應用了,那爲何還須要「應用上下文」對象呢?對於單應用單請求來講,使用「請求上下文」確實就能夠了。然而,Flask的設計理念之一就是多應用的支持。當在一個應用的請求上下文環境中,須要嵌套處理另外一個應用的相關操做時,「請求上下文」顯然就不能很好地解決問題了。如何讓請求找到「正確」的應用呢?咱們可能會想到,能夠再增長一個請求上下文環境,並將其推入_request_ctx_stack棧中。因爲兩個上下文環境的運行是獨立的,不會相互干擾,因此經過調用_request_ctx_stack.top.app或者魔法current_app也能夠得到當前上下文環境正在處理哪一個應用。這種辦法在必定程度上可行,可是若是對於第二個應用的處理不涉及到相關請求,那也就無從談起「請求上下文」。

爲了應對這個問題,Flask中將應用相關的信息單獨拿出來,造成一個「應用上下文」對象。這個對象能夠和「請求上下文」一塊兒使用,也能夠單獨拿出來使用。不過有一點須要注意的是:在建立「請求上下文」時必定要建立一個「應用上下文」對象。有了「應用上下文」對象,即可以很容易地肯定當前處理哪一個應用,這就是魔法current_app。在0.1版本中,current_app是對_request_ctx_stack.top.app的引用,而在0.9版本中current_app是對_app_ctx_stack.top.app的引用。

下面以一個多應用的例子進行說明:

# example - Flask v0.9
>>> from flask import Flask, _request_ctx_stack, _app_ctx_stack
# 建立兩個Flask應用
>>> app = Flask(__name__)
>>> app2 = Flask(__name__)
# 先查看兩個棧中的內容
>>> _request_ctx_stack._local.__storage__
{}
>>> _app_ctx_stack._local.__storage__
{}
# 構建一個app的請求上下文環境,在這個環境中運行app2的相關操做
>>> with app.test_request_context():
        print "Enter app's Request Context:"
        print _request_ctx_stack._local.__storage__
        print _app_ctx_stack._local.__storage__
        print
        with app2.app_context():
            print "Enter app2's App Context:"
            print _request_ctx_stack._local.__storage__
            print _app_ctx_stack._local.__storage__
            print
            # do something
        print "Exit app2's App Context:"
        print _request_ctx_stack._local.__storage__
        print _app_ctx_stack._local.__storage__
        print
# Result
Enter app's Request Context:
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>]}}

Enter app2's App Context:
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>, <flask.ctx.AppContext object at 0x0000000007313198>]}}

Exit app2's App Context
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>]}}

在以上的例子中:

  • 咱們首先建立了兩個Flask應用appapp2

  • 接着咱們構建了一個app的請求上下文環境。當進入這個環境中時,這時查看兩個棧的內容,發現兩個棧中已經有了當前請求的請求上下文對象和應用上下文對象。而且棧頂的元素都是app的請求上下文和應用上下文;

  • 以後,咱們再在這個環境中嵌套app2的應用上下文。當進入app2的應用上下文環境時,兩個上下文環境便隔離開來,此時再查看兩個棧的內容,發現_app_ctx_stack中推入了app2的應用上下文對象,而且棧頂指向它。這時在app2的應用上下文環境中,current_app便會一直指向app2

  • 當離開app2的應用上下文環境,_app_ctx_stack棧便會銷燬app2的應用上下文對象。這時查看兩個棧的內容,發現兩個棧中只有app的請求的請求上下文對象和應用上下文對象。

  • 最後,離開app的請求上下文環境後,兩個棧便會銷燬app的請求的請求上下文對象和應用上下文對象,棧爲空。

與上下文對象有關的「全局變量」

在Flask中,爲了更加方便地處理一些變量,特意提出了「全局變量」的概念。這些全局變量有:

# Flask v0.9
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_object, 'request'))
session = LocalProxy(partial(_lookup_object, 'session'))
g = LocalProxy(partial(_lookup_object, 'g'))

# 輔助函數
def _lookup_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of request context')
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return top.app

能夠看出,Flask中使用的一些「全局變量」,包括current_apprequestsessiong等都來自於上下文對象。其中current_app一直指向_app_ctx_stack棧頂的「應用上下文」對象,是對當前應用的引用。而requestsessiong等一直指向_request_ctx_stack棧頂的「請求上下文」對象,分別引用請求上下文的requestsessiong。不過,從 Flask 0.10 起,對象 g 存儲在應用上下文中而再也不是請求上下文中。

另一個問題,在造成這些「全局變量」的時候,使用了werkzeug.local模塊的LocalProxy類。之因此要用該類,主要是爲了動態地實現對棧頂元素的引用。若是不使用這個類,在生成上述「全局變量」的時候,它們由於指向棧頂元素,而棧頂元素此時爲None,因此這些變量也會被設置爲None常量。後續即便有上下文對象被推入棧中,相應的「全局變量」也不會發生改變。爲了動態地實現對棧頂元素的引用,這裏必須使用werkzeug.local模塊的LocalProxy類。

http://www.javashuo.com/article/p-wphqskdv-ha.html

http://www.javashuo.com/article/p-nrpuaavc-bv.html

相關文章
相關標籤/搜索