詳解Flask上下文

Yabea發表於2020-08-02

上下文是在Flask開發中的一個核心概念,本文將通過閱讀原始碼分享下其原理和實現。

Flask系列文章

  1. Flask開發初探
  2. WSGI到底是什麼
  3. Flask原始碼分析一:服務啟動
  4. Flask路由內部實現原理
  5. Flask容器化部署原理與實現
  6. Flask許可權管理

首先,什麼是Flask中的上下文?

在Flask中,對一個請求進行處理時,檢視函式一般都會需要請求引數、配置等物件,當然不能對每個請求都傳參一層層到檢視函式(這顯然很不優雅嘛),為此,設計出了上下文機制(比如像我們經常會呼叫的request就是上下文變數)。

Flask中提供了兩種上下文:

  1. 請求上下文:包括request和session,儲存請求相關的資訊
  2. 程式上下文:包括current_app和g,為了更好的分離程式的狀態,應用起來更加靈活,方便調測等

這四個是上下文變數具體的作用是什麼?

  1. request:封裝客戶端傳送的請求報文資料
  2. session:用於記住請求之間的資料,通過簽名的cookie實現,常用來記住使用者登入狀態
  3. current_app:指向處理請求的當前程式例項,比如獲取配置,經常會用current_app.config
  4. g:當前請求中的全域性變數,因為程式上下文的生命週期是伴隨請求上下文產生和銷燬的,所以每次請求都會重設。一般我會在結合鉤子函式在請求處理前使用。

具體是怎麼實現的呢?

上下文具體的實現檔案:ctx.py

請求上下文物件通過RequestContext類實現,當Flask程式收到請求時,會在wsgi_app()中呼叫Flask.request_context(),例項化RequestContext()作為請求上下文物件,接著會通過push()方法將請求資料推入到請求上下文堆疊(LocalStack),然後通過full_dispatch_request物件執行檢視函式,呼叫完成之後通過auto_pop方法來移除。所以,請求上下文的生命週期開始於呼叫wsgi_app()時,結束與響應生成之後。具體程式碼:

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

程式上下文物件通過AppContext類實現,程式上下文的建立方式有兩種:

  1. 自動建立:在處理請求時,程式上下文會隨著請求上下文一起被建立
  2. 手動建立:with語句

通過閱讀原始碼,可以看到上面兩個上下文物件的push和pop都是通過操作LocalStack物件實現的,那麼,LocalStack是怎樣實現的呢?

Werkzeug的LocalStack是棧結構,在 globals.py中定義:

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

具體的實現:

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

可以看到:

  1. LocalStack實現了棧的push、pop和獲取棧頂資料的top資料
  2. 整個類基於Local類,在建構函式中建立Local類的例項_local,資料是push到Werkzeug提供的Local類中
  3. 定義__call__方法,當例項被呼叫直接返回棧頂物件的Werkzeug提供的LocalProxy代理,即LocalProxy例項,所以,_request_ctx_stack_app_ctx_stack都是代理。

看到這裡,就有以下問題:

Local類是怎樣儲存資料的呢?為啥需要儲存到Local中?

先看下程式碼:

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

可以看到,Local建構函式中定義了兩個屬性:

  1. __storage__:用來儲存每個執行緒的真實資料,對應的儲存結構為->{執行緒ID:{name:value}}
  2. __ident_func__:通過get_ident()方法獲取執行緒ID,可以看到優先會使用Greenlet獲取協程ID,其次是thread模組的執行緒ID

Local類在儲存資料的同時,記錄對應的執行緒ID,獲取資料時根據當前執行緒的id即可獲取到對應資料,這樣就保證了全域性使用的上下文物件不會在多個執行緒中產生混亂,保證了每個執行緒中上下文物件的獨立和準確。

可以看到,Local類例項被呼叫時也同樣的被包裝成了一個LocalProxy代理,為什麼要用LocalProxy代理?

代理是一種設計模式,通過建立一個代理物件來操作實際物件,簡單理解就是使用一箇中間人來轉發操作,Flask上下文處理為什麼需要它?

看下程式碼實現:

@implements_bool
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__'):
            object.__setattr__(self, '__wrapped__', local)

    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__)
            
	@property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')

    def __repr__(self):
        try:
            obj = self._get_current_object()
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__
        return repr(obj)

    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False

    def __unicode__(self):
        try:
            return unicode(self._get_current_object())  # noqa
        except RuntimeError:
            return repr(self)

    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]
    ...

通過__getattr__()__setitem__()__delitem__會動態的更新例項物件。

再結合上下文物件的呼叫:

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上下文機制的原理有了清晰的認識。

相關文章