1. 程式人生 > 程式設計 >Flask原始碼剖析(四):Flask的上下文機制(下)

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

前言

本文緊接著「Flask原始碼剖析(三):Flask的上下文機制(上)」,討論如下問題。

  • 1.Python中有thread.local了,werkzeug為什麼還要自己弄一個Local類來儲存資料?
  • 2.為什麼不構建一個上下文而是要將其分為請求上下文(request context)和應用上下文(application context)?
  • 3.為什麼不直接使用Local?而要通過LocalStack類將其封裝成棧的操作?
  • 4.為什麼不直接使用LocalStack?而要通過LocalProxy類來代理操作?

回顧Flask上下文

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

所謂Flask上下文,其實就是基於list實現的棧,這個list存放在Local類例項化的物件中,Local類利用執行緒id作為字典的key,執行緒具體的值作為字典的values來實現執行緒安全,使用的過程就是出棧入棧的過程,此外,在具體使用時,會通過LocalProxy類將操作都代理給Local類物件。

為何需要werkzeug庫的Local類?

treading標準庫中已經提供了local物件,該物件實現的效果與Local類似,以執行緒id為字典的key,將執行緒具體的值作為字典的values儲存,簡單使用如下。

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時,使用者可以單獨構建新的上下文,將一個上下文以請求上下文和應用上下文的形式分開,可以讓使用者單獨建立其中一種上下文,這很方便使用者在不同的情景使用不同的上下文。

為什麼要使用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,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 原始碼解析:上下文