Flask中上下文棧(context stacks)的目的?

[回答者Mark Hildreth]

Multiple Apps 多個應用

Flask可以有多個應用,如果沒有瞭解到這一點,應用上下文的作用確實會令人迷惑。考慮一下這種場景:你想在一個WSGI python解釋器運行多個Flask應用。這裏我們講的不是藍本,而是完全不同的Flask應用。
一個應用分發(Application Dispatching)的例子

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

注意,這裏有兩個完全不同的Flask應用:frontend與backend(前端與後端)
換句話說,通過調用兩次Flask(…)的構造方法來創建兩個flask應用實例

Contexts 上下文

當你使用Flask時,需要經常使用全局變量訪問不同的函數。例如,你可能會讀到下邊的代碼:

from flask import request

在某個視圖中,你可能會使用request對象訪問當前請求的信息。顯然,request不是全局變量。實際上,它是一個context local值。這裏做了一個特殊處理,使得當我們請求request.path時能正確的從當前request的對象中獲取path屬性。

實際上,即使在Flask中運行多個線程,Flask也能夠保持request對象的隔離性。在這種情況下,不同的線程可以處理各自的請求,併發的獲取request.path信息,而且確保在他們各自的請求中能獲取到正確的信息。

Putting it Together 綜合考慮

我們已經看到Flask可以在同一個解釋器中處理多個應用,這是由於Flask允許你使用”context local”全局變量來實現的。這其中肯定是有某種機制決定究竟哪個是當前的request對象。

綜合以上情況,Flask一定有某種方法判斷當前的應用是哪個。
你可能會碰到如下代碼:

from flask import url_for

與request例子類似,url_for函數邏輯上是取決於當前環境。在這種情形下,可以十分肯定的說這種邏輯與Flask認定哪個app是當前的app有着緊密的關係。在上邊的frontend/backend例子中,frontend與backend的app中可能都包含/login路由,url_for(‘/login’)會爲不同app返回不同的值,這取決於視圖正在處理的request(frontend或backend)

To answer your questions… 書歸正傳,回答問題

當我們談到request或者應用上下文時,棧的目的是什麼?

Request Context 文檔:
由於在request上下文中維護者一個棧,因此你可以多次地出棧和入棧。這種方式十分有利於實現內部的重定向轉發。
換句話說,你可以將多個內部的重定向請求放入“當前”requests或“當前”應用的棧中。

下邊給出一個例子,你可以使request返回“內部重定向”的結果。例如一個用戶對A發起請求,並將它返回的結果傳給用戶B。大多數情況下,你會給這個用戶生成一個重定向請求,將這個用戶重新定向到資源B,這意味着用戶要運行第二個請求去獲取B。與此對比。略微有些不同的處理方式是使用內部重定向,當處理A時,Flask會爲資源B本身生成一個新的請求,將第二個請求的結果作爲用戶原始請求的結果。(筆者附註:不同體現在,第一種方法是程序員自己創建第二個request請求,而第二種是Flask框架自動創建這種request,並返回正確的結果給我們)

問題:這兩個棧是分別獨立的還是他們是一個棧的兩部分?
他們是兩個獨立的棧。在任何時候你可以獲得“當前”的應用或請求(位於棧的頂部)。源碼:

flask.globals.py
……
#contextlocals
_request_ctx_stack=LocalStack()    #request 上下文棧
_app_ctx_stack=LocalStack()   # Flask應用上下文棧

問題:request上下文是被推進棧中,還是其本身就是一個棧?
一個請求上下文是request 上下文棧(_request_ctx_stack)的元素。“應用上下文”與”app context stack”之間的關係與之類似。

問題:可以在兩個棧的頂部push/pop多個上下文嗎?如果可以,何種情況下需要這麼做?
在Flask應用中,你通常不需要這麼做。舉個例子,你可能會在處理內部重定向時想要這麼做。但是,即使在這種情形下,你也應該利用Flask處理新請求,Flask會爲你做好所有的push/pop操作。

有一些情形下你需要自己進行棧操作

Running code outside of a request 在請求之外運行代碼

一個典型的例子是 ,當使用 Flask-SQLAlchemy擴展設置SQL數據庫或者定義模型時。當使用類似下方的代碼時,

app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)

我們需要在shell中使用app和db的值時:

from myapp import app, db
# Set up models
db.create_all()

在這種情況下,Flask-SQLAlchemy擴展需要知道app應用的信息,但是當執行create_all()時,會拋出一個沒有上下文的錯誤。

RuntimeError: application not registered on db instance and no application bound to current context

這個錯誤是正常的,這是由於在運行create_all()時,你沒有告訴Flask這個應用需要處理的信息。
你可能疑惑,爲何在視圖中你運行相似的函數,沒有使用with app.app_context()也沒有出現錯誤呢。原因是,當你處理實際的web請求時,flask已經替你自動管理好應用的上下文。此類問題只會出現在:當有代碼運行在視圖函數(或類似的回調函數)以外的情形下。

解決方案是自己將應用上下文推入棧中,例如:

from myapp import app, db
# Set up models
with app.app_context():
    db.create_all()

測試

另一個需要手動操作棧的地方是測試。你可以創建一個單元測試用來處理請求和檢查結果:

import unittest
from flask import request
class MyTest(unittest.TestCase):
    def test_thing(self):
        with app.test_request_context('/?next=http://example.com/') as ctx:
            # 你可以在這查看請求上下文棧的屬性
#此處請求上下文棧會是空的

[回答者mike_e]

每個http請求都要創建一個上下文(線程),這是必須創建Local線程的原因。這樣可以通過維護特定的request上下文,來保證request和g這樣的對象可以被全局訪問。此外,Flask在處理Http請求時,可以從內部模擬request,這就必須要求在一個棧中存儲他們各自的上下文。Flask允許多個WSGI應用運行在單一進程中,並且在一個request請求中可能調用不止一個應用,因此必須爲應用設計一個上下文棧。
我們首先來理解一下werkzeug如何實現Local線程。
Local
當發起一個 http請求時,某個線程的上下文會處理這個請求。也就是說,在http請求發起的同時會產生一個新的上下文。Werkzeug(__version__='0.12.2') 允許使用greenlets替代python的原生線程。如果沒有安裝greenlets會使用threads代替。每個線程都有一個唯一id作爲標誌符,get_ident()函數提供線程的檢索功能。這個函數隱藏在request, current_app,url_for, g,等上下文綁定的全局對象中。

try:
    from greenlet import get_ident
except ImportError:
    from thread import get_ident

通過線程檢索函數get_ident,我們可以很容易的知道當前的線程。我們可以創建一個叫做Local的線程,Local是一個上下文對象,可以被全局的訪問。你可以訪問特定線程的屬性值。
例如:

# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'

在同一時刻,可以通過訪問Local來獲取這兩個線程的屬性值。當查詢local.first_name時,線程1的上下文會返回’John’,而線程2會返回’Debbie’

這是如何做到的呢?我們看一下Local的源碼:

werkzeug.local.py
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        # 初始化線程字典__storage__,鍵:線程id,值:線程
        object.__setattr__(self, '__storage__', {})
        #get_ident 函數生成線程的Id
        object.__setattr__(self, '__ident_func__', get_ident)

     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} # 設置屬性值

從上面的代碼可以看到,Local使用字典storage存儲線程和相應的線程id.
鍵:線程id,值:線程。初始化時綁定字典和線程id生成函數。getattr是從字典中根據id取出線程。 setattr將特定的線程放入字典中(或者更新已有的值)。在Flask中並沒有使用Local對象,使用的是LocalProxy 對象。

LocalProxy
class LocalProxy(object):
    def __init__(self, local, name):
       # local這裏是一個實際的Local對象,可以用來查找特定的對象,其標識符是name.
       #local是可以調用的,可以確定代理對象
       self.local = local
       # 'name'作爲標識符,傳遞給local來查找特定的對象
       self.name = name

    def _get_current_object(self):
        #如果self.local是一個Local對象,則其已經實現了__release_local__()方法,
        #正如其名字一樣,通常用來釋放Local對象
        #這裏通過簡單的查找來標記哪個是實際的Local對象,哪個是可調用的對象
        if hasattr(self.local, '__release_local__'):
        try:
            return getattr(self.local, self.name)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.name)

        #如果self.local不是一個Local對象,則一定是可調用的對象
        #,這樣可以決定用戶感興趣的對象
        return self.local(self.name)

    #現在LocalProxy 執行其特定的職責
    #比如,在Local中代理一個對象,我們將感興趣的對象的魔幻方法
    #全部交給代理來處理
    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')
    def __repr__(self):
        try:
            return repr(self._get_current_object())
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__
    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False
    # ...  等等 ... 
    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]
    # ... 等等 ...
    __setattr__ = lambda x, n, v: 
                  setattr(x._get_current_object(), n, v)
    __delattr__ = lambda x, n: 
                    delattr(x._get_current_object(), n)
    __str__ = lambda x: str(x._get_current_object())
    __lt__ = lambda x, o: x._get_current_object() < o
    __le__ = lambda x, o: x._get_current_object() <= o
    __eq__ = lambda x, o: x._get_current_object() == o
# ...  等等 …

現在你可以這樣創建全局訪問代理對象
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')

在request請求的前期,你可以在local(之前創建的代理)中存儲一些對象,無論哪個線程都可以訪問:

# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()

使用LocalProxy 爲全局訪問對象,相對直接使用Local對象而言,可以簡化對象管理。你可以爲一個單一的Local對象創建許多全局代理對象。在request請求的末期(清理階段),你可以簡單的釋放一個Local(對其storage執行pop操作),並且這種操作不會影響代理,這些代理對象仍然可以全局的訪問,並處理隨後的http請求。

# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()

當我們已經有一個Local對象時,爲了簡化創建代理LocalProxy ,Werkzeug 實現Local.call()方法的過程如下:

class Local(object):
    # ... 
    # ... all same stuff as before go here ...
    # ... 
    def __call__(self, name):
        return LocalProxy(self, name)
# now you can do
local = Local()
request = local('request')
g = local('g')

然而,如果查看Flask源碼(flask.globals.py),你仍然無法知道request, g, current_app 和session等對象時如何創建。當我們創建應用時,Flask會同時創建多個”假的”request請求(從一個真正的http請求)並且在過程中push多個應用上下文。由於這些”併發”的請求和應用在任一時刻只有一個被處理。因此,有必要使用一個棧來儲存他們各自的上下文。當一個新的請求產生或者一個應用被調用時,他們會將上下文push進各自的棧中。Flask使用LocalStack目的正是如此。當他們結束自己的業務時會從棧中彈出上下文。

LocalStack

class LocalStack(object):
    def __init__(self):
        self.local = Local()
    def push(self, obj):
        """壓入棧中一個新的元素"""
        rv = getattr(self.local, 'stack', None)
        if rv is None:
            self.local.stack = rv = []
        rv.append(obj)
        return rv
    def pop(self):
        """移除棧頂的元素, 返回舊值或None.
            """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self.local) # 釋放 local
            return stack[-1]
        else:
            return stack.pop()
    @property
    def top(self):
        """T獲取棧頂的元素 """
        try:
            return self.local.stack[-1]
        except (AttributeError, IndexError):
            return None

注意,LocalStack是一個駐存在Local對象裏的棧,並不是存儲Local對象的棧。這意味着,儘管這個棧是全局可以訪問的,但是其在彼此的線程中是不同的。
Flask並沒有直接的從LocalStack中獲取request, current_app, g, 和session等對象,而是包裝了一層查找功能來尋找潛在的對象。

class LocalStack(object):
    …..
    """__version__='0.12.2'"""
    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)
    …

#flask.globals.py   [__version__='0.12.2']

def _lookup_req_object(name):   #在request上下文棧中查找屬性
    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'))

所有的上述對象,在應用建立的開始,棧中不會有任何的對象。除非你將一個request或者應用的上下文壓入到他們各自的棧中。

如果你對上下文是如何插入棧中的細節感興趣,可以參考源碼flask.app.Flask.wsgi_app()。當登錄到wsgi的應用時web服務器將http環境參數傳遞給request請求,隨後創建RequestContext 對象。然後調用push()方法將上下文壓入_request_ctx_stack棧中。一旦push到棧的頂部,就可以全局的訪問_request_ctx_stack.top。這裏列出上述流程的部分代碼:
建立一個WSGI應用:

app = Flask(*config, **kwconfig)
# ...

隨後http請求到達服務器,WSGI服務器調用app:

app(environ, start_response) # aka app.__call__(environ, start_response)
app中大致的處理過程如下:
class Flask(object):
# ...
def __call__(self, environ, start_response):
   return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
   ctx = RequestContext(self, environ)
   ctx.push()
   try:
       # process the request here
       # raise error if any
       # return Response
   finally:
       ctx.pop()
# …

隨後RequestContext處理代碼如下:

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.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()
        self.flashes = None

    def push(self):
        _request_ctx_stack.push(self)

    def pop(self):
        _request_ctx_stack.pop()

當request 請求完成初始化後,接着在視圖函數功能中查找request.path。隨後的處理步驟如下:

• 流程的起點是全局可訪問的LocalProxy 對象
• 在_lookup_req_object函數中查找特定的對象
• _lookup_req_object函數在_request_ctx_stack棧的頂部查找對象
• 爲了查找頂部的上下文,LocalStack 對象首先檢索內部的Local屬性(self._local),並設置Localstack屬性
• 從stack中獲取頂top的上下文
• top.request可以決定客戶端感興趣的對象
• 從這個對象中獲取path屬性

我們已經知道Local, LocalProxy, 和 LocalStack的工作機制,現在總結一下:
request 對象是一個簡單的全局可訪問的對象。它是一個代理對象,儲存在Local對象的屬性stack中,即存儲在stack的棧頂。


參考文獻:

[1]https://stackoverflow.com/questions/20036520/what-is-the-purpose-of-flasks-context-stacks/20041823#20041823

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