flask上下文

在web程序運行時,可能會有許多請求,這些請求中包含了許多信息,比如url、參數、路由、請求方式等等。 這些信息在視圖函數中可能會被用到,它們就是上下文。那麼如何保存這些上下文,到了需要的時候再調用呢?因爲這些請求是動態的,flask需要動態地訪問它們。可能我說的這些不太好理解,下面看例子:

from flask import Flask
from flask import request,current_app

app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

@app.route('/')
def index():
    url=request.url
    config = current_app.config
    return "hello world"

@app.route('/sample',methods=['GET','POST'])
def sample():
    url = request.url
    app.config['SECRET_KEY'] = 'very important key'
    config = current_app.config
    return "test"

app.run()

開啓調試模式運行上面這段代碼時,我分別訪問了本地的https://127.0.0.1:5000/https://127.0.0.1:5000/sample,調試過程部分值如下。
這裏寫圖片描述
這裏寫圖片描述

從上面的兩張圖片我們可以清楚地看到,因爲請求不同,所以request和current_app也有所變化。但是我們一開始就從flask裏面導入了request和current_app,那麼flask是怎麼做到動態改變這兩個上下文的值的呢?(注:flask有兩種上下文,一個是application context,一種是request context。這兩種上下文又分別被分爲兩種。application context被分爲current app和g,request context被分爲request和session,這四個值都是全局變量。)

先來看一部分源碼。這部分源碼在app.py中,這兩個函數都是類Flask方法。一旦有請求出現,call函數被調用,然後執行wsgi_app()函數,在wsgi_app函數中,有這麼一句: ctx = self.request_context(environ)。循跡追蹤下去,request_context函數返回了一個RequestContext實例。根據後面的ctx.push()可以猜測,在push函數中,RequestContext對象將兩種上下文壓棧。至於RequestContext怎麼實現的後面再詳細講。

def __call__(self, environ, start_response):
    return self.wsgi_app(environ, start_response)

def wsgi_app(self, environ, start_response):
    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:
        error = sys.exc_info()[1]
        raise
    return response(environ, start_response)
finally:
        if self.should_ignore_error(error):
        error = None
        ctx.auto_pop(error)
def request_context(self, environ):
    return RequestContext(self, environ)

那麼,它們是怎麼被調用的呢?這就要分析werkzeug的源碼了,因爲flask上下文這部分的實現和werkzeug聯繫很緊密。

這部分實現,我們從werkzeug中的Local類開始分析。我們看到,Local類有兩個屬性,storage是一個列表,列表中的每一個元素都是字典。字典的鍵值是當前線程協程的id,字典的值則又是一個字典,這個子字的鍵值,在下面我們會看到,會有兩種,一種是request,一種是session,值就是當前線程或協程的上下文。Local類用getattrsetattr以及delattr分別完成了對列表元素的訪問、添加、刪除。

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

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    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)

但是這裏還僅僅是做到了用線程id作爲唯一標識,組成id:{‘request’:req1}這樣的鍵值對,並保存到列表裏,並沒有實現棧以及動態訪問。werkzeug提供了兩個類,進一步實現了這個功能。如下所示,這兩個類分別是LocalStack和LocalProxy。LocalStack這個類,將Local類的實例作爲自己的屬性,實現了請求上下文的入棧出棧、棧頂元素訪問。(push函數第二參數obj就是請求上下文)。LocalProxy則是把Local類封裝了一層,實現了動態訪問請求上下文。

class LocalStack(object):

    def __init__(self):
        self._local = Local()

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

    def _get__ident_func__(self):
        return self._local.__ident_func__

    def _set__ident_func__(self, value):
        object.__setattr__(self._local, '__ident_func__', value)
    __ident_func__ = property(_get__ident_func__, _set__ident_func__)
    del _get__ident_func__, _set__ident_func__

    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)

    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


class LocalProxy(object):

    __slots__ = ('__local', '__dict__', '__name__', '__wrapped__')

    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)
        if callable(local) and not hasattr(local, '__release_local__'):
            # "local" is a callable that is not an instance of Local or
            # LocalManager: mark it as a wrapped function.
            object.__setattr__(self, '__wrapped__', local)

    def _get_current_object(self):
        """Return the current object.  This is useful if you want the real
        object behind the proxy at a time for performance reasons or because
        you want to pass the object into a different context.
        """
        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 __dir__(self):
        try:
            return dir(self._get_current_object())
        except RuntimeError:
            return []

    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

    def __delitem__(self, key):
        del self._get_current_object()[key]

分析到了這裏,也沒看見這這兩個類到底是怎樣實現動態獲取上下文的,別急。看看下面的代碼。下面的代碼是flask框架globals.py部分的代碼,這部分代碼被設置爲了全局的。在這裏面我們看到current_app、request、session、g四個上下文。你會不會想起,有時候我們會用到這樣的代碼:from flask import current_app,request,現在我們知道它們是從哪裏來的了。但是問題似乎還沒有解決,至少目前看來我們還是不知道flask是怎麼做的。

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

還記得前面的ResponseContext類嗎?在前面給出的代碼ctx = self.request_context(environ) ctx.push()我們推測,正是在這裏,上下文進棧。實際上request_context函數返回的是ResonseContext實例。在下面的代碼中我們可以看到push函數。在push函數中獲取了_app_ctx_stack的棧頂元素(application context),如果棧頂元素爲空或者棧頂元素中的app不是當前app,就把當前的application context入棧。然後再把request context入棧。注意,這裏入棧的兩個push , _request_ctx_stack.push(self) 的self是ResponseContext類, app_ctx.push() 入棧的是ApplicationContext類。

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

        self._implicit_app_ctx_stack = []
        self.preserved = False
        self._preserved_exc = None
        self._after_request_functions = []

        self.match_request()

    def _get_g(self):
        return _app_ctx_stack.top.g
    def _set_g(self, value):
        _app_ctx_stack.top.g = value
    g = property(_get_g, _set_g)
    del _get_g, _set_g

    def match_request(self):
        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):
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        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)

        if self.session is None:
            session_interface = self.app.session_interface
            self.session = session_interface.open_session(
                self.app, self.request
            )

            if self.session is None:
                self.session = session_interface.make_null_session(self.app)

    def pop(self, exc=_sentinel):
        app_ctx = self._implicit_app_ctx_stack.pop()

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

                if hasattr(sys, 'exc_clear'):
                    sys.exc_clear()

                request_close = getattr(self.request, 'close', None)
                if request_close is not None:
                    request_close()
                clear_request = True
        finally:
            rv = _request_ctx_stack.pop()

            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)

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

    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)

至此脈絡應該很清晰明瞭了,通過語句 from flask import current_app,request 在一開始就導入到程序中去的,current_app和request,只是一個被初始化的實例,這個時候,它們的棧是空棧。等到客戶端向服務器發送請求,請求被轉發到flask框架,然後入棧。在調用的時候如:url = request.url ,就會去取出棧頂元素,返回相應的值。這樣一來,就可以做到在視圖函數中動態處理請求。依靠的就是上下文。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章