LocalStack和Local對象實現棧的管理

Flask的上下文

flask 中有兩種上下文:application context 和 request context。上下文有關的內容定義在 globals.py 文件

# 關鍵處,後面會解釋
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
 
 
# 請求上下文棧對象
_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'))

這裏的實現用到了兩個東西:LocalStack 和 LocalProxy。它們兩個的結果就是咱們能夠動態地獲取兩個上下文的內容,在併發程序中每一個視圖函數都會看到屬於本身的上下文,而不會出現混亂。

LocalStack 和 LocalProxy 都是 werkzeug 提供的,定義在 local.py 文件中,分析以前,先了解一個相似於threading.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):
        # 數據保存在 __storage__ 中,產生了{'__storage__':{}}結構, 後續訪問都是對該屬性的操做
        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)
 
    # 用來清空(析構)當前線程或者協程的數據(狀態)
    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)
 
    # 下面三個方法實現了屬性的訪問、設置和刪除。
    # 注意到,內部都調用 `self.__ident_func__` 獲取當前線程或者協程的 id,而後再訪問對應的內部字典。
    # 若是訪問或者刪除的屬性不存在,會拋出 AttributeError。
    # 這樣,外部用戶看到的就是它在訪問實例的屬性,徹底不知道字典或者多線程/協程切換的實現
    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類的做用就是產生下面的數據結構
    {'__storage__':{
        '線程或者協程的 id':{name:value}
    }}  
`

瞭解了Local類後,回來分析LocalStack 和 LocalProxy

LocalStack 是基於 Local 實現的棧結構。若是說 Local 提供了多線程或者多協程隔離的屬性訪問,那麼 LocalStack 就提供了隔離的棧訪問

class LocalStack(object):
 
    def __init__(self):
        # 實例化Local類,並封裝到LocalStack類中
        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 三個方法實現了棧的操做,
    # 能夠看到棧的數據是保存在 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):
        
        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

咱們能夠看到最上面有一行代碼,他就是實例化一個LocalStack對象:

# 實例化 請求上下文棧對象
_request_ctx_stack = LocalStack()

# 它會把當前線程或者協程的請求都保存在棧裏,等使用的時候再從裏面讀取

LocalProxy 是一個 Local 對象的代理,負責把全部對本身的操做轉發給內部的 Local 對象。

class LocalProxy(object):
    
    __slots__ = ('__local', '__dict__', '__name__')
 
    def __init__(self, local, name=None):
        # 產生了{'__local':local對象}結構,爲何_LocalProxy__local會變成__local,請參考面向對象的私有屬性
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)
 
    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 __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

初步瞭解了LocalStack和LocalProxy類後,還要了解一個小知識點:

偏函數:
import functools
# 偏函數
def func(a1,a2):
    print(a1,a2)
# 幫func函數傳遞參數,併產生新的函數名
new_func = functools.partial(func,123)
# 新的函數只須要傳一個參數
new_func(2)

如今再看下面的代碼

_request_ctx_stack = LocalStack()
# 調用 _lookup_req_object方法,拿到封裝在 請求上下文 裏的request對象
request = LocalProxy(partial(_lookup_req_object, 'request'))
# 調用 _lookup_req_object方法,拿到封裝在 請求上下文 裏的session對象
session = LocalProxy(partial(_lookup_req_object, 'session'))

若是你在疑惑 :何時請求上下文裏封裝了request對象和session對象?那你可記得以前的wsgi_app方法:

def wsgi_app(self, environ, start_response):
    # 先建立RequestContext對象,並封裝了request對象
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # ctx.push的內部完成了什麼?
            # 1. 先創建了_request_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
                # 完成功能代碼: RequestContext/push方法裏的 top = _request_ctx_stack.top
            # 2. 再建立_app_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
                # 完成功能代碼: RequestContext/push方法裏的 app_ctx = _app_ctx_stack.top
            # 3. 再建立AppContext對象並封裝了app和g
                # 完成功能代碼: RequestContext/push方法裏的 app_ctx = self.app.app_context()
            # 4. 把AppContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[app_ctx對象]}}}數據結構
                # 完成功能代碼: app_ctx.push()
            # 5. 把RequestContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[ctx對象]}}}數據結構
                # 完成功能代碼: _request_ctx_stack.push(self)
            # 6. RequestContext對象封裝session
                # 完成功能代碼: self.session = session_interface.open_session(self.app, self.request)
            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)

ctx.push()的詳細分析:

# RequestContext類中push方法
    def push(self):
        #1. 先創建了_request_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
        top = _request_ctx_stack.top
        # 2. 再建立_app_ctx_stack對象(LocalStack),暫時內部數據結構{"__storage__":{}}}
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            # 3. 再建立AppContext對象並封裝了app和g
            app_ctx = self.app.app_context()
            # 4. 把AppContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[app_ctx對象]}}}數據結構
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)
        # 5. 把RequestContext對象壓入到棧中,從而造成了{"__storage__":{線程ID:{'stack':[ctx對象]}}}數據結構
        _request_ctx_stack.push(self)

        if self.session is None:
            session_interface = self.app.session_interface
            # 6. RequestContext對象封裝session
            self.session = session_interface.open_session(self.app, self.request)
            if self.session is None:
                self.session = session_interface.make_null_session(self.app)
        if self.url_adapter is not None:
            # 實現了路由的匹配邏輯
            self.match_request()

首先從下面兩行代碼開始:

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

每次在調用 app.__call__ 的時候,都會把對應的請求信息壓棧,最後執行完請求的處理以後把它出棧。

咱們來看看request_context, 這個 方法只return一個類:

def request_context(self, environ):
    # 調用了 RequestContext,並把 self 和請求信息的字典 environ 當作參數傳遞進去
    return RequestContext(self, environ)
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.match_request()

    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
        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):
        """ 和push相反,把 請求上下文和應用上下文 有關的信息從各自對應的棧上刪除"""
        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()
            if clear_request:
                rv.request.environ['werkzeug.request'] = None
            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)

每一個 request context 都保存了當前請求的信息,好比 request 對象和 app 對象,初始化的最後調用了 match_request 實現了路由的匹配邏輯

push 操做就是把該請求的 ApplicationContext(若是 _app_ctx_stack 棧頂不是當前請求所在 app ,須要建立新的 app context) 和 RequestContext 有關的信息保存到對應的棧上,壓棧後還會保存 session 的信息; pop 則相反,把 request context 和 application context 出棧,作一些清理性的工做。

結論:

`
每次有請求過來的時候,flask 會先建立當前線程或者進程須要處理的兩個重要上下文對象,把它們保存到隔離的棧裏面,這樣視圖函數進行處理的時候就能直接從棧上獲取這些信息
`

上面分析事後,你可能有三個疑惑:

# 1. 不會產生多個app嗎?
    `不會,此處app對象運用了單例模式,因此只有一個app對象,所以多個 request 共享了 application context`
# 2. 爲何要把 請求上下文 和 應用上下文 分開?每一個請求不是都同時擁有這兩個上下文信息嗎?
    `由於'靈活性',雖然在實際運行中,每一個請求對應一個 請求上下文 和一個 應用上下文,可是在測試或者 python shell 中運行的時候,用戶能夠單首創建 請求上下文 或者 應用上下文,這種靈活度方便用戶的不一樣的使用場景;`
# 3. 爲何 請求上下文 和 應用上下文 都有實現成棧的結構?每一個請求難道會出現多個 請求上下文 或者 應用上下文 嗎?
    `在web runtime 時,棧永遠只有1個對象。可是在寫離線腳本時,纔會用在棧中放多個對象.(建立一個py文件本地運行)`

第三個問題的代碼示例:

# --------------------------例1------------------------------------
from flask import current_app,g
from pro_excel import create_app

app1 = create_app()
with app1.app_context():  # AppContext對象(app,g) -> local對象
    print(current_app.config) # -1 top app1 
    app2 = create_app()
    with app2.app_context():  # AppContext對象(app,g) -> local對象
        print(current_app.config) # top -1 app2 
    print(current_app.config) # top -1 app1

# 寫離線腳本且多個上下文嵌套時,纔會在棧中添加多個對象。


# ---------------------------例2-----------------------------------
from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
 
application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})
# 使用 werkzeug 的 DispatcherMiddleware 實現多個 app 的分發,這種狀況下 _app_ctx_stack 棧裏會出現兩個 application context。
相關文章
相關標籤/搜索