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