4.flask 源碼解析:上下文
上下文(application context 和 request context)
上下文一直是計(jì)算機(jī)中難理解的概念,在知乎的一個(gè)問(wèn)題下面有個(gè)很通俗易懂的回答:
每一段程序都有很多外部變量纫塌。只有像Add這種簡(jiǎn)單的函數(shù)才是沒(méi)有外部變量的。一旦你的一段程序有了外部變量传货,這段程序就不完整,不能獨(dú)立運(yùn)行。你為了使他們運(yùn)行藤抡,就要給所有的外部變量一個(gè)一個(gè)寫(xiě)一些值進(jìn)去辫封。這些值的集合就叫上下文硝枉。
– vzch
比如,在 flask 中倦微,視圖函數(shù)需要知道它執(zhí)行情況的請(qǐng)求信息(請(qǐng)求的 url妻味,參數(shù),方法等)以及應(yīng)用信息(應(yīng)用中初始化的數(shù)據(jù)庫(kù)等)欣福,才能夠正確運(yùn)行责球。
最直觀地做法是把這些信息封裝成一個(gè)對(duì)象,作為參數(shù)傳遞給視圖函數(shù)。但是這樣的話雏逾,所有的視圖函數(shù)都需要添加對(duì)應(yīng)的參數(shù)嘉裤,即使該函數(shù)內(nèi)部并沒(mé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)多線程訪問(wèn)某個(gè)變量的時(shí)候只看到自己的數(shù)據(jù)骇笔。內(nèi)部的原理說(shuō)起來(lá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
又演化出來(lái)兩個(gè)變量 current_app
和 g
签财,而 request context
則演化出來(lái) 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ù)訪問(wèn)都是對(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)了屬性的訪問(wèn)氓拼、設(shè)置和刪除你画。
# 注意到抵碟,內(nèi)部都調(diào)用 `self.__ident_func__` 獲取當(dāng)前線程或者協(xié)程的 id,然后再訪問(wèn)對(duì)應(yīng)的內(nèi)部字典撬即。
# 如果訪問(wèn)或者刪除的屬性不存在立磁,會(huì)拋出 AttributeError。
# 這樣剥槐,外部用戶看到的就是它在訪問(wèn)實(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ì)沿后。用戶訪問(wèn)實(shí)例的屬性,就變成了訪問(wèn)內(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__
,用來(lái)清空(析構(gòu))當(dāng)前線程或者協(xié)程的數(shù)據(jù)(狀態(tài))造锅。__call__
操作來(lái)創(chuàng)建一個(gè) LocalProxy
對(duì)象撼唾,LocalProxy
會(huì)在下面講到。
理解了 Local
哥蔚,我們繼續(xù)回來(lái)看另外兩個(gè)類倒谷。
LocalStack
是基于 Local
實(shí)現(xiàn)的棧結(jié)構(gòu)。如果說(shuō) Local
提供了多線程或者多協(xié)程隔離的屬性訪問(wèn)糙箍,那么 LocalStack
就提供了隔離的棧訪問(wèn)渤愁。下面是它的實(shí)現(xiàn)代碼,可以看到它提供了 push
深夯、pop
和 top
方法猴伶。
__release_local__
可以用來(lái)清空當(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)鍵是把通過(guò)參數(shù)傳遞進(jìn)來(lái)的 Local
實(shí)例保存在 __local
屬性中顾孽,并定義了 _get_current_object()
方法獲取當(dāng)前線程或者協(xié)程對(duì)應(yīng)的對(duì)象祝钢。
NOTE:前面雙下劃線的屬性,會(huì)保存到 _ClassName__variable
中若厚。所以這里通過(guò) “_LocalProxy__local”
設(shè)置的值拦英,后面可以通過(guò) self.__local
來(lái)獲取。關(guān)于這個(gè)知識(shí)點(diǎn)测秸,可以查看 stackoverflow 的這個(gè)問(wèn)題疤估。
然后 LocalProxy
重寫(xiě)了所有的魔術(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ù)來(lái)獲取保存在里面的 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)求的處理之后把它出棧吧碾。
我們來(lái)看看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)求過(guò)來(lái)的時(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è)疑惑沒(méi)有解答萝毛。
- 為什么要把 request context 和 application context 分開(kāi)?每個(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í)候巴粪,用戶可以單獨(dú)創(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芭届。
Update: 為什么要用 LocalProxy
寫(xiě)完這篇文章之后,收到有位讀者的疑問(wèn):為什么要使用 LocalProxy
感耙?不使用 LocalProxy
直接訪問(wèn) LocalStack
的對(duì)象會(huì)有什么問(wèn)題嗎褂乍?
這是個(gè)很好的問(wèn)題,上面也確實(shí)沒(méi)有很明確地給出這個(gè)答案即硼。這里解釋一下树叽!
首先明確一點(diǎn),Local
和 LocalStack
實(shí)現(xiàn)了不同線程/協(xié)程之間的數(shù)據(jù)隔離谦絮。在為什么用 LocalStack
而不是直接使用 Local
的時(shí)候题诵,我們說(shuō)過(guò)這是因?yàn)?flask 希望在測(cè)試或者開(kāi)發(fā)的時(shí)候,允許多 app 层皱、多 request 的情況性锭。而 LocalProxy
也是因?yàn)檫@個(gè)才引入進(jìn)來(lái)的!
我們拿 current_app = LocalProxy(_find_app)
來(lái)舉例子叫胖。每次使用 current_app
的時(shí)候草冈,他都會(huì)調(diào)用 _find_app
函數(shù),然后對(duì)得到的變量進(jìn)行操作瓮增。
如果直接使用 current_app = _find_app()
有什么區(qū)別呢怎棱?區(qū)別就在于,我們導(dǎo)入進(jìn)來(lái)之后绷跑,current_app
就不會(huì)再變化了拳恋。如果有多 app 的情況,就會(huì)出現(xiàn)錯(cuò)誤砸捏,比如:
from flask import current_app
app = create_app()
admin_app = create_admin_app()
def do_something():
with app.app_context():
work_on(current_app)
with admin_app.app_context():
work_on(current_app)
這里我們出現(xiàn)了嵌套的 app谬运,每個(gè) with 上下文都需要操作其對(duì)應(yīng)的 app
,如果不適用 LocalProxy
是做不到的垦藏。
對(duì)于 request
也是類似梆暖!但是這種情況真的很少發(fā)生,有必要費(fèi)這么大的功夫增加這么多復(fù)雜度嗎掂骏?
其實(shí)還有一個(gè)更大的問(wèn)題轰驳,這個(gè)例子也可以看出來(lái)。比如我們知道 current_app
是動(dòng)態(tài)的弟灼,因?yàn)樗澈髮?duì)應(yīng)的棧會(huì) push 和 pop 元素進(jìn)去级解。那剛開(kāi)始的時(shí)候,棧一定是空的袜爪,只有在 with app.app_context()
這句的時(shí)候蠕趁,才把棧數(shù)據(jù) push 進(jìn)去。而如果不采用 LocalProxy
進(jìn)行轉(zhuǎn)發(fā)辛馆,那么在最上面導(dǎo)入 from flask import current_app
的時(shí)候俺陋,current_app
就是空的,因?yàn)檫@個(gè)時(shí)候還沒(méi)有把數(shù)據(jù) push 進(jìn)去昙篙,后面調(diào)用的時(shí)候根本無(wú)法使用腊状。
所以為什么需要 LocalProxy
呢?簡(jiǎn)單總結(jié)一句話:因?yàn)樯舷挛谋4娴臄?shù)據(jù)是保存在棧里的苔可,并且會(huì)動(dòng)態(tài)發(fā)生變化缴挖。如果不是動(dòng)態(tài)地去訪問(wèn),會(huì)造成數(shù)據(jù)訪問(wèn)異常焚辅。