這是 flask 原始碼解析系列文章的其中一篇,本系列所有文章列表:
上下文(application context 和 request context)
上下文一直是計算機中難理解的概念,在知乎的一個問題下面有個很通俗易懂的回答:
每一段程式都有很多外部變數。只有像Add這種簡單的函式才是沒有外部變數的。一旦你的一段程式有了外部變數,這段程式就不完整,不能獨立執行。你為了使他們執行,就要給所有的外部變數一個一個寫一些值進去。這些值的集合就叫上下文。
— vzch
比如,在 flask 中,檢視函式需要知道它執行情況的請求資訊(請求的 url,引數,方法等)以及應用資訊(應用中初始化的資料庫等),才能夠正確執行。
最直觀地做法是把這些資訊封裝成一個物件,作為引數傳遞給檢視函式。但是這樣的話,所有的檢視函式都需要新增對應的引數,即使該函式內部並沒有使用到它。
flask 的做法是把這些資訊作為類似全域性變數的東西,檢視函式需要的時候,可以使用 from flask import request
獲取。但是這些物件和全域性變數不同的是——它們必須是動態的,因為在多執行緒或者多協程的情況下,每個執行緒或者協程獲取的都是自己獨特的物件,不會互相干擾。
那麼如何實現這種效果呢?如果對 python 多執行緒比較熟悉的話,應該知道多執行緒中有個非常類似的概念 threading.local
,可以實現多執行緒訪問某個變數的時候只看到自己的資料。內部的原理說起來也很簡單,這個物件有一個字典,儲存了執行緒 id 對應的資料,讀取該物件的時候,它動態地查詢當前執行緒 id 對應的資料。flaskpython 上下文的實現也類似,後面會詳細解釋。
flask 中有兩種上下文:application context
和 request context
。上下文有關的內容定義在 globals.py
檔案,檔案的內容也非常短:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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 # 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')) |
flask
提供兩種上下文:application context
和 request context
。app lication context
又演化出來兩個變數 current_app
和 g
,而 request context
則演化出來 request
和 session
。
這裡的實現用到了兩個東西:LocalStack
和 LocalProxy
。它們兩個的結果就是我們可以動態地獲取兩個上下文的內容,在併發程式中每個檢視函式都會看到屬於自己的上下文,而不會出現混亂。
LocalStack
和 LocalProxy
都是 werkzeug
提供的,定義在 local.py
檔案中。在分析這兩個類之前,我們先介紹這個檔案另外一個基礎的類 Local
。Local
就是實現了類似 threading.local
的效果——多執行緒或者多協程情況下全域性變數的隔離效果。下面是它的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# 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 class Local(object): __slots__ = ('__storage__', '__ident_func__') def __init__(self): # 資料儲存在 __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__
屬性的,這個屬性變數是個巢狀的字典:map[ident]map[key]value
。最外面字典 key 是執行緒或者協程的 identity,value 是另外一個字典,這個內部字典就是使用者自定義的 key-value 鍵值對。使用者訪問例項的屬性,就變成了訪問內部的字典,外面字典的 key 是自動關聯的。__ident_func
是 協程的 get_current
或者執行緒的 get_ident
,從而獲取當前程式碼所線上程或者協程的 id。
除了這些基本操作之外,Local
還實現了 __release_local__
,用來清空(析構)當前執行緒或者協程的資料(狀態)。__call__
操作來建立一個 LocalProxy
物件,LocalProxy
會在下面講到。
理解了 Local
,我們繼續回來看另外兩個類。
LocalStack
是基於 Local
實現的棧結構。如果說 Local
提供了多執行緒或者多協程隔離的屬性訪問,那麼 LocalStack
就提供了隔離的棧訪問。下面是它的實現程式碼,可以看到它提供了 push
、pop
和 top
方法。
__release_local__
可以用來清空當前執行緒或者協程的棧資料,__call__
方法返回當前執行緒或者協程棧頂元素的代理物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
class LocalStack(object): """This class works similar to a :class:`Local` but keeps a stack of objects instead. """ def __init__(self): 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): """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 |
我們在之前看到了 request context
的定義,它就是一個 LocalStack
的例項:
1 |
_request_ctx_stack = LocalStack() |
它會當前執行緒或者協程的請求都儲存在棧裡,等使用的時候再從裡面讀取。至於為什麼要用到棧結構,而不是直接使用 Local
,我們會在後面揭曉答案,你可以先思考一下。
LocalProxy
是一個 Local
物件的代理,負責把所有對自己的操作轉發給內部的 Local
物件。LocalProxy
的建構函式介紹一個 callable 的引數,這個 callable 呼叫之後需要返回一個 Local
例項,後續所有的屬性操作都會轉發給 callable 返回的物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class LocalProxy(object): """Acts as a proxy for a werkzeug local. Forwards all operations to a proxied object. """ __slots__ = ('__local', '__dict__', '__name__') def __init__(self, local, name=None): object.__setattr__(self, '_LocalProxy__local', local) object.__setattr__(self, '__name__', name) def _get_current_object(self): """Return the current object.""" 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 |
這裡實現的關鍵是把通過引數傳遞進來的 Local
例項儲存在 __local
屬性中,並定義了 _get_current_object()
方法獲取當前執行緒或者協程對應的物件。
NOTE:前面雙下劃線的屬性,會儲存到 _ClassName__variable
中。所以這裡通過 “_LocalProxy__local”
設定的值,後面可以通過 self.__local
來獲取。關於這個知識點,可以檢視 stackoverflow 的這個問題。
然後 LocalProxy
重寫了所有的魔術方法(名字前後有兩個下劃線的方法),具體操作都是轉發給代理物件的。這裡只給出了幾個魔術方法,感興趣的可以檢視原始碼中所有的魔術方法。
繼續回到 request context
的實現:
1 2 3 |
_request_ctx_stack = LocalStack() request = LocalProxy(partial(_lookup_req_object, 'request')) session = LocalProxy(partial(_lookup_req_object, 'session')) |
再次看這段程式碼希望能看明白,_request_ctx_stack
是多執行緒或者協程隔離的棧結構,request
每次都會呼叫 _lookup_req_object
棧頭部的資料來獲取儲存在裡面的 requst context
。
那麼請求上下文資訊是什麼被放在 stack 中呢?還記得之前介紹的 wsgi_app()
方法有下面兩行程式碼嗎?
1 2 |
ctx = self.request_context(environ) ctx.push() |
每次在呼叫 app.__call__
的時候,都會把對應的請求資訊壓棧,最後執行完請求的處理之後把它出棧。
我們來看看request_context
, 這個 方法只有一行程式碼:
1 2 |
def request_context(self, environ): return RequestContext(self, environ) |
它呼叫了 RequestContext
,並把 self
和請求資訊的字典 environ
當做引數傳遞進去。追蹤到 RequestContext
定義的地方,它出現在 ctx.py
檔案中,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
class RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the `_request_ctx_stack` and removed at the end of it. It will create the URL adapter and request object for the WSGI environment provided. """ 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): """Can be overridden by a subclass to hook into the matching of the request. """ 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): """Binds the request context to the current context.""" # Before we push the request context we have to ensure that there # is an application context. 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): """Pops the request context and unbinds it by doing that. This will also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. """ 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() # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. if clear_request: rv.request.environ['werkzeug.request'] = None # Get rid of the app as well if necessary. 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 會先建立當前執行緒或者程式需要處理的兩個重要上下文物件,把它們儲存到隔離的棧裡面,這樣檢視函式進行處理的時候就能直接從棧上獲取這些資訊。
NOTE:因為 app 例項只有一個,因此多個 request
共享了 application context
。
到這裡,關於 context 的實現和功能已經講解得差不多了。還有兩個疑惑沒有解答。
- 為什麼要把 request context 和 application context 分開?每個請求不是都同時擁有這兩個上下文資訊嗎?
- 為什麼 request context 和 application context 都有實現成棧的結構?每個請求難道會出現多個 request context 或者 application context 嗎?
第一個答案是“靈活度”,第二個答案是“多 application”。雖然在實際執行中,每個請求對應一個 request context 和一個 application context,但是在測試或者 python shell 中執行的時候,使用者可以單獨建立 request context 或者 application context,這種靈活度方便使用者的不同的使用場景;而且棧可以讓 redirect 更容易實現,一個處理函式可以從棧中獲取重定向路徑的多個請求資訊。application 設計成棧也是類似,測試的時候可以新增多個上下文,另外一個原因是 flask 可以多個 application 同時執行:
1 2 3 4 5 6 7 |
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。
參考資料
請使用手機”掃一掃”x