Flask源碼剖析(四):Flask的上下文機制(下)

前言

本文緊接着「Flask源碼剖析(三):Flask的上下文機制(上)」,討論以下問題。前端

  • 1.Python中有thread.local了,werkzeug爲何還要本身弄一個Local類來存儲數據?
  • 2.爲何不構建一個上下文而是要將其分爲請求上下文(request context)和應用上下文(application context)?
  • 3.爲何不直接使用Local?而要經過LocalStack類將其封裝成棧的操做?
  • 4.爲何不直接使用LocalStack?而要經過LocalProxy類來代理操做?

回顧Flask上下文

在上一篇文章中,詳細討論了Flask上下文機制,這裏簡單回顧一下。python

所謂Flask上下文,其實就是基於list實現的棧,這個list存放在Local類實例化的對象中,Local類利用線程id做爲字典的key,線程具體的值做爲字典的values來實現線程安全,使用的過程就是出棧入棧的過程,此外,在具體使用時,會經過LocalProxy類將操做都代理給Local類對象。shell

爲什麼須要werkzeug庫的Local類?

treading標準庫中已經提供了local對象,該對象實現的效果與Local相似,以線程id爲字典的key,將線程具體的值做爲字典的values存儲,簡單使用以下。flask

In [1]: import threading

In [2]: local = threading.local()

In [3]: local.name = '二兩'

In [4]: local.name
Out[4]: '二兩'
複製代碼

那爲什麼werkzeug庫要本身再實現一個功能相似的Local類呢?後端

主要緣由是爲了兼容協程,當用戶經過greenlet庫來構建協程時,由於多個協程能夠在同一個線程中,threading.local沒法處理這種狀況,而Local能夠經過getcurrent()方法來獲取協程的惟一標識。瀏覽器

# werkzeug/local.py

# 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
複製代碼

爲何要將上下文分爲多個?

回顧一下問題。安全

爲何不構建一個上下文而是要將其分爲請求上下文(request context)和應用上下文(application context)?服務器

爲了「靈活度」。數據結構

雖然在實際的Web項目中,每一個請求只會對應一個請求上下文和應用上下文,但在debug或使用flask shell時,用戶能夠單獨構建新的上下文,將一個上下文以請求上下文和應用上下文的形式分開,可讓用戶單首創建其中一種上下文,這很方便用戶在不一樣的情景使用不一樣的上下文。app

爲何要使用LocalStack?

回顧一下問題。

爲何不直接使用Local?而要經過LocalStack類將其封裝成棧的操做?

在StackoverFlow上能夠搜到相應的答案。總結而言,經過LocalStack實現棧結構而不直接使用Local的目的是爲了在多應用情景下讓一個請求能夠很簡單的知道當前上下文是哪一個。

要理解這個回答,先要回顧一下Flask多應用開發的內容並將其與上下文的概念結合在一塊兒理解。

Flask多應用開發的簡單例子以下。

from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.serving import run_simple
from flask import Flask

frontend = Flask('frontend')
backend = Flask('backend')

@frontend.route('/home')
def home():
    return 'frontend home'

@backend.route('/home')
def home():
    return 'backend home'

"""默認使用frontend,訪問 127.0.0.1:5000/home 返回 frontend url以forntend開頭時,使用frontend, 訪問 127.0.0.1:5000/frontend/home 返回 frontend url以backend開頭時,使用backend 訪問 127.0.0.1:5000/backend/home 返回 backend"""
app = DispatcherMiddleware(frontend, {
    '/frontend':     frontend
    '/backend':     backend
})

if __name__ == '__main__':
    run_simple('127.0.0.1', 5000, app)
複製代碼

利用werkzeug的DispatcherMiddleware,讓一個Python解釋器能夠同時運行多個獨立的應用實例,其效果雖然跟使用藍圖時的效果相似,但要注意,此時是多個獨立的Flask應用,具體而言,每一個獨立的Flask應用都建立了本身的上下文。

每一個獨立的Flask應用都是一個合法的WSGI應用,利用DispatcherMiddleware,經過調度中間件的邏輯將多個Flask應用組合成一個大應用。

簡單理解Flask多應用後,回顧一下Flask上下文的做用。好比,要得到當前請求的path屬性,能夠經過以下方式。

from flask import request

print(request.path)
複製代碼

Flask在多應用的狀況下,依舊能夠經過request.path得到當前應用的信息,實現這個效果的前提就是,Flask知道當前請求對應的上下文。

棧結構很好的實現了這個前提,每一個請求,其相關的上下文就在棧頂,直接將棧頂上下文出棧就能夠得到當前請求對應上下文中的信息了。

有點抽象?以上面的Flask多應用的代碼舉個具體的例子。

在上面Flask多應用的代碼中,構建了frontend應用與backend應用,兩個應用相互獨立,分別負責前端邏輯與後端邏輯,經過DispatcherMiddleware將其整合在一塊兒,這種狀況下,_app_ctx_stack棧中就會有兩個應用上下文。

訪問127.0.0.1:5000/backend/home時,backend應用上下文入棧,成爲棧頂。想要獲取當前請求中的信息時,直接出棧就能夠得到與當前請求對應的上下文信息。

須要注意,請求上下文、應用上下文是具體的對象,而_request_ctx_stack(請求上下文棧)與_app_ctx_stack(應用上下文棧)是數據結構,再次看一下LocalStack類關於建立棧的代碼。

# werkzeug/local.py

class LocalStack(object):
    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()
複製代碼

能夠發現,所謂棧就是一個list,結合Local類的代碼,上下文堆棧其結構大體爲{thread.get_ident(): []},每一個線程都有獨立的一個棧。

此外,Flask基於棧結構能夠很容易實現內部重定向。

  • 外部重定向:用戶經過瀏覽器請求URL-1後,服務器返回302重定向請求,讓其請求URL-2,用戶的瀏覽器會發起新的請求,請求新的URL-2,得到相應的數據。
  • 內部重定向:用戶經過瀏覽器請求URL-1後,服務器內部之間將ULR-2對應的信息直接返回給用戶。

Flask在內部經過屢次入棧出棧的操做能夠很方便的實現內部重定向。

爲何要使用LocalProxy?

回顧一下問題。

爲何不直接使用LocalStack?而要經過LocalProxy類來代理操做?

這是由於Flask的上下文中保存的數據都是存放在棧裏而且會動態變化的,經過LocalProxy能夠動態的訪問相應的對象,從而避免形成數據訪問異常。

怎麼理解?看一個簡單的例子,首先,直接操做LocalStack,代碼以下。

from werkzeug.local import LocalStack

l_stack = LocalStack()
l_stack.push({'name': 'ayuliao'})
l_stack.push({'name': 'twotwo'})

def get_name():
    return l_stack.pop()

name = get_name()

print(f"name is {name['name']}")
print(f"name is {name['name']}")
複製代碼

運行上述代碼,輸出的結果以下。

name is twotwo
name is twotwo
複製代碼

能夠發現,結果相同。

利用LocalProxy代理操做,代碼以下。

from werkzeug.local import LocalStack, LocalProxy

l_stack = LocalStack()
l_stack.push({'name': 'ayuliao'})
l_stack.push({'name': 'twotwo'})

def get_name():
    return l_stack.pop()

# 代理操做get_name
name2 = LocalProxy(get_name)
print(f"name is {name2['name']}")
print(f"name is {name2['name']}")
複製代碼

運行上述代碼,輸出的結果以下。

name is twotwo
name is ayuliao
複製代碼

經過LocalProxy代理操做後,結果不一樣。

經過LocalProxy代理操做後,每一次獲取值的操做其實都會調用__getitem__,該方法是個匿名函數,x就是LocalProxy實例自己,這裏即爲name2,而i則爲查詢的屬性,這裏即爲name。

class LocalProxy(object):    
    # ... 省略部分代碼
    __getitem__ = lambda x, i: x._get_current_object()[i]
複製代碼

結合__init___get_current_object()方法來看。

class LocalProxy(object): 
    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() # 再次執行get_name
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.__name__
複製代碼

__init__方法中,將get_name賦值給了_LocalProxy__local,由於get_name不存在__release_local__屬性,此時使用_get_current_object()方法,至關於再次執行ge_name(),出棧後得到新的值。

經過上面的分析,明白了經過LocalProxy代理後,調用兩次name['name']獲取的值不一樣的緣由。

那爲何要這樣作?看到Flask中globals.py的部分代碼。

# flask/globals.py

current_app = LocalProxy(_find_app)
複製代碼

當前應用current_app是經過LocalProxy(_find_app)得到的,即每次調用current_app()會執行出棧操做,得到與當前請求相對應的上下文信息。

若是current_app = _find_app(),此時current_app就不會再變化了,在多應用多請求的狀況下是不合理的,會拋出相應的異常。

總結

最後,以簡單的話來總結一下上面的討論。

問:Python中有thread.local了,werkzeug爲何還要本身弄一個Local類來存儲數據?

答:werkzeug的Local類支持協程。

問:爲何不構建一個上下文而是要將其分爲請求上下文(request context)和應用上下文(application context)?

答:爲了「靈活度」。

問:爲何不直接使用Local?而要經過LocalStack類將其封裝成棧的操做?

答:爲了在多應用情景下讓一個請求能夠很簡單的知道當前上下文是哪一個。此外棧的形式易於Flask內部重定向等操做的實現。

問:爲何不直接使用LocalStack?而要經過LocalProxy類來代理操做?

答:由於Flask的上下文中保存的數據都是存放在棧裏而且會動態變化的,經過LocalProxy能夠動態的訪問相應的對象。

結尾

Flask上下文的內容就介紹完了,其實主要的邏輯在Werkzeug上,討論了Local、LocalStack、LocalProxy,後面將繼續剖析Flask源碼,但願喜歡。

若是文章對你有啓發、有幫助,點擊「在看」支持一下二兩,讓我有分享的動力。

參考

What is the purpose of Flask's context stacks?

Flask上下文相關文檔

flask 源碼解析:上下文

相關文章
相關標籤/搜索