摘要:但是這些對象和全局變量不同的是它們必須是動態(tài)的,因?yàn)樵诙嗑€程或者多協(xié)程的情況下,每個線程或者協(xié)程獲取的都是自己獨(dú)特的對象,不會互相干擾。中有兩種上下文和。就是實(shí)現(xiàn)了類似的效果多線程或者多協(xié)程情況下全局變量的隔離效果。
這是 flask 源碼解析系列文章的其中一篇,本系列所有文章列表:
flask 源碼解析:簡介
flask 源碼解析:應(yīng)用啟動流程
flask 源碼解析:路由
flask 源碼解析:上下文
flask 源碼解析:請求
上下文(application context 和 request context)上下文一直是計(jì)算機(jī)中難理解的概念,在知乎的一個問題下面有個很通俗易懂的回答:
每一段程序都有很多外部變量。只有像Add這種簡單的函數(shù)才是沒有外部變量的。一旦你的一段程序有了外部變量,這段程序就不完整,不能獨(dú)立運(yùn)行。你為了使他們運(yùn)行,就要給所有的外部變量一個一個寫一些值進(jìn)去。這些值的集合就叫上下文。
-- vzch
比如,在 flask 中,視圖函數(shù)需要知道它執(zhí)行情況的請求信息(請求的 url,參數(shù),方法等)以及應(yīng)用信息(應(yīng)用中初始化的數(shù)據(jù)庫等),才能夠正確運(yùn)行。
最直觀地做法是把這些信息封裝成一個對象,作為參數(shù)傳遞給視圖函數(shù)。但是這樣的話,所有的視圖函數(shù)都需要添加對應(yīng)的參數(shù),即使該函數(shù)內(nèi)部并沒有使用到它。
flask 的做法是把這些信息作為類似全局變量的東西,視圖函數(shù)需要的時候,可以使用 from flask import request 獲取。但是這些對象和全局變量不同的是——它們必須是動態(tài)的,因?yàn)樵诙嗑€程或者多協(xié)程的情況下,每個線程或者協(xié)程獲取的都是自己獨(dú)特的對象,不會互相干擾。
那么如何實(shí)現(xiàn)這種效果呢?如果對 python 多線程比較熟悉的話,應(yīng)該知道多線程中有個非常類似的概念 threading.local,可以實(shí)現(xiàn)多線程訪問某個變量的時候只看到自己的數(shù)據(jù)。內(nèi)部的原理說起來也很簡單,這個對象有一個字典,保存了線程 id 對應(yīng)的數(shù)據(jù),讀取該對象的時候,它動態(tài)地查詢當(dāng)前線程 id 對應(yīng)的數(shù)據(jù)。flaskpython 上下文的實(shí)現(xiàn)也類似,后面會詳細(xì)解釋。
flask 中有兩種上下文:application context 和 request context。上下文有關(guān)的內(nèi)容定義在 globals.py 文件,文件的內(nèi)容也非常短:
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。
這里的實(shí)現(xiàn)用到了兩個東西:LocalStack 和 LocalProxy。它們兩個的結(jié)果就是我們可以動態(tài)地獲取兩個上下文的內(nèi)容,在并發(fā)程序中每個視圖函數(shù)都會看到屬于自己的上下文,而不會出現(xiàn)混亂。
LocalStack 和 LocalProxy 都是 werkzeug 提供的,定義在 local.py 文件中。在分析這兩個類之前,我們先介紹這個文件另外一個基礎(chǔ)的類 Local。Local 就是實(shí)現(xiàn)了類似 threading.local 的效果——多線程或者多協(xié)程情況下全局變量的隔離效果。下面是它的代碼:
# 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): # 數(shù)據(jù)保存在 __storage__ 中,后續(xù)訪問都是對該屬性的操作 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) # 清空當(dāng)前線程/協(xié)程保存的所有數(shù)據(jù) def __release_local__(self): self.__storage__.pop(self.__ident_func__(), None) # 下面三個方法實(shí)現(xiàn)了屬性的訪問、設(shè)置和刪除。 # 注意到,內(nèi)部都調(diào)用 `self.__ident_func__` 獲取當(dāng)前線程或者協(xié)程的 id,然后再訪問對應(yīng)的內(nèi)部字典。 # 如果訪問或者刪除的屬性不存在,會拋出 AttributeError。 # 這樣,外部用戶看到的就是它在訪問實(shí)例的屬性,完全不知道字典或者多線程/協(xié)程切換的實(shí)現(xiàn) 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 對象內(nèi)部的數(shù)據(jù)都是保存在 __storage__ 屬性的,這個屬性變量是個嵌套的字典:map[ident]map[key]value。最外面字典 key 是線程或者協(xié)程的 identity,value 是另外一個字典,這個內(nèi)部字典就是用戶自定義的 key-value 鍵值對。用戶訪問實(shí)例的屬性,就變成了訪問內(nèi)部的字典,外面字典的 key 是自動關(guān)聯(lián)的。__ident_func 是 協(xié)程的 get_current 或者線程的 get_ident,從而獲取當(dāng)前代碼所在線程或者協(xié)程的 id。
除了這些基本操作之外,Local 還實(shí)現(xiàn)了 __release_local__ ,用來清空(析構(gòu))當(dāng)前線程或者協(xié)程的數(shù)據(jù)(狀態(tài))。__call__ 操作來創(chuàng)建一個 LocalProxy 對象,LocalProxy 會在下面講到。
理解了 Local,我們繼續(xù)回來看另外兩個類。
LocalStack 是基于 Local 實(shí)現(xiàn)的棧結(jié)構(gòu)。如果說 Local 提供了多線程或者多協(xié)程隔離的屬性訪問,那么 LocalStack 就提供了隔離的棧訪問。下面是它的實(shí)現(xiàn)代碼,可以看到它提供了 push、pop 和 top 方法。
__release_local__ 可以用來清空當(dāng)前線程或者協(xié)程的棧數(shù)據(jù),__call__ 方法返回當(dāng)前線程或者協(xié)程棧頂元素的代理對象。
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 三個方法實(shí)現(xiàn)了棧的操作, # 可以看到棧的數(shù)據(jù)是保存在 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 的實(shí)例:
_request_ctx_stack = LocalStack()
它會當(dāng)前線程或者協(xié)程的請求都保存在棧里,等使用的時候再從里面讀取。至于為什么要用到棧結(jié)構(gòu),而不是直接使用 Local,我們會在后面揭曉答案,你可以先思考一下。
LocalProxy 是一個 Local 對象的代理,負(fù)責(zé)把所有對自己的操作轉(zhuǎn)發(fā)給內(nèi)部的 Local 對象。LocalProxy 的構(gòu)造函數(shù)介紹一個 callable 的參數(shù),這個 callable 調(diào)用之后需要返回一個 Local 實(shí)例,后續(xù)所有的屬性操作都會轉(zhuǎn)發(fā)給 callable 返回的對象。
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
這里實(shí)現(xiàn)的關(guān)鍵是把通過參數(shù)傳遞進(jìn)來的 Local 實(shí)例保存在 __local 屬性中,并定義了 _get_current_object() 方法獲取當(dāng)前線程或者協(xié)程對應(yīng)的對象。
NOTE:前面雙下劃線的屬性,會保存到 _ClassName__variable 中。所以這里通過 “_LocalProxy__local” 設(shè)置的值,后面可以通過 self.__local 來獲取。關(guān)于這個知識點(diǎn),可以查看 stackoverflow 的這個問題。
然后 LocalProxy 重寫了所有的魔術(shù)方法(名字前后有兩個下劃線的方法),具體操作都是轉(zhuǎn)發(fā)給代理對象的。這里只給出了幾個魔術(shù)方法,感興趣的可以查看源碼中所有的魔術(shù)方法。
繼續(xù)回到 request context 的實(shí)現(xiàn):
_request_ctx_stack = LocalStack() request = LocalProxy(partial(_lookup_req_object, "request")) session = LocalProxy(partial(_lookup_req_object, "session"))
再次看這段代碼希望能看明白,_request_ctx_stack 是多線程或者協(xié)程隔離的棧結(jié)構(gòu),request 每次都會調(diào)用 _lookup_req_object 棧頭部的數(shù)據(jù)來獲取保存在里面的 requst context。
那么請求上下文信息是什么被放在 stack 中呢?還記得之前介紹的 wsgi_app() 方法有下面兩行代碼嗎?
ctx = self.request_context(environ) ctx.push()
每次在調(diào)用 app.__call__ 的時候,都會把對應(yīng)的請求信息壓棧,最后執(zhí)行完請求的處理之后把它出棧。
我們來看看request_context, 這個 方法只有一行代碼:
def request_context(self, environ): return RequestContext(self, environ)
它調(diào)用了 RequestContext,并把 self 和請求信息的字典 environ 當(dāng)做參數(shù)傳遞進(jìn)去。追蹤到 RequestContext 定義的地方,它出現(xiàn)在 ctx.py 文件中,代碼如下:
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 都保存了當(dāng)前請求的信息,比如 request 對象和 app 對象。在初始化的最后,還調(diào)用了 match_request 實(shí)現(xiàn)了路由的匹配邏輯。
push 操作就是把該請求的 ApplicationContext(如果 _app_ctx_stack 棧頂不是當(dāng)前請求所在 app ,需要創(chuàng)建新的 app context) 和 RequestContext 有關(guān)的信息保存到對應(yīng)的棧上,壓棧后還會保存 session 的信息; pop 則相反,把 request context 和 application context 出棧,做一些清理性的工作。
到這里,上下文的實(shí)現(xiàn)就比較清晰了:每次有請求過來的時候,flask 會先創(chuàng)建當(dāng)前線程或者進(jìn)程需要處理的兩個重要上下文對象,把它們保存到隔離的棧里面,這樣視圖函數(shù)進(jìn)行處理的時候就能直接從棧上獲取這些信息。
NOTE:因?yàn)?app 實(shí)例只有一個,因此多個 request 共享了 application context。
到這里,關(guān)于 context 的實(shí)現(xiàn)和功能已經(jīng)講解得差不多了。還有兩個疑惑沒有解答。
為什么要把 request context 和 application context 分開?每個請求不是都同時擁有這兩個上下文信息嗎?
為什么 request context 和 application context 都有實(shí)現(xiàn)成棧的結(jié)構(gòu)?每個請求難道會出現(xiàn)多個 request context 或者 application context 嗎?
第一個答案是“靈活度”,第二個答案是“多 application”。雖然在實(shí)際運(yùn)行中,每個請求對應(yīng)一個 request context 和一個 application context,但是在測試或者 python shell 中運(yùn)行的時候,用戶可以多帶帶創(chuàng)建 request context 或者 application context,這種靈活度方便用戶的不同的使用場景;而且??梢宰?redirect 更容易實(shí)現(xiàn),一個處理函數(shù)可以從棧中獲取重定向路徑的多個請求信息。application 設(shè)計(jì)成棧也是類似,測試的時候可以添加多個上下文,另外一個原因是 flask 可以多個 application 同時運(yùn)行:
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 實(shí)現(xiàn)多個 app 的分發(fā),這種情況下 _app_ctx_stack 棧里會出現(xiàn)兩個 application context。
參考資料advanced flask patterns by Armin Ronacher
Flask doc: The application context
Flask 的 Context 機(jī)制
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/38453.html
摘要:我們知道響應(yīng)分為三個部分狀態(tài)欄版本狀態(tài)碼和說明頭部以冒號隔開的字符對,用于各種控制和協(xié)商服務(wù)端返回的數(shù)據(jù)。 這是 flask 源碼解析系列文章的其中一篇,本系列所有文章列表: flask 源碼解析:簡介 flask 源碼解析:應(yīng)用啟動流程 flask 源碼解析:路由 flask 源碼解析:上下文 flask 源碼解析:請求 flask 源碼解析:響應(yīng) response 簡介 在 f...
摘要:可以看到,雖然是同樣的請求數(shù)據(jù),在不同的階段和不同組件看來,是完全不同的形式。請求還有一個不那么明顯的特性它不能被應(yīng)用修改,應(yīng)用只能讀取請求的數(shù)據(jù)。 這是 flask 源碼解析系列文章的其中一篇,本系列所有文章列表: flask 源碼解析:簡介 flask 源碼解析:應(yīng)用啟動流程 flask 源碼解析:路由 flask 源碼解析:上下文 flask 源碼解析:請求 flask 源碼解...
摘要:中有一個非常重要的概念每個應(yīng)用都是一個可調(diào)用的對象。它規(guī)定了的接口,會調(diào)用,并傳給它兩個參數(shù)包含了請求的所有信息,是處理完之后需要調(diào)用的函數(shù),參數(shù)是狀態(tài)碼響應(yīng)頭部還有錯誤信息。一般來說,嵌套的最后一層是業(yè)務(wù)應(yīng)用,中間就是。 文章屬于作者原創(chuàng),原文發(fā)布在個人博客。 WSGI 所有的 python web 框架都要遵循 WSGI 協(xié)議,如果對 WSGI 不清楚,可以查看我之前的介紹文章。 ...
摘要:上次遺留了兩個問題先說一下自己的看法問題明明一個線程只能處理一個請求那么棧里的元素永遠(yuǎn)是在棧頂那為什么需要用棧這個結(jié)構(gòu)用普通變量不行嗎和都是線程隔離的那么為什么要分開我認(rèn)為在的情況下是可以不需要棧這個結(jié)構(gòu)的即使是單線程下也不需要原本我以為在 上次遺留了兩個問題,先說一下自己的看法問題:1.明明一個線程只能處理一個請求,那么棧里的元素永遠(yuǎn)是在棧頂,那為什么需要用棧這個結(jié)構(gòu)?用普通變量不行...
閱讀 3206·2021-09-22 15:05
閱讀 2763·2019-08-30 15:56
閱讀 1071·2019-08-29 17:09
閱讀 803·2019-08-29 15:12
閱讀 2084·2019-08-26 11:55
閱讀 3070·2019-08-26 11:52
閱讀 3381·2019-08-26 10:29
閱讀 1385·2019-08-23 17:19