Flask中的請求上下文和應(yīng)用上下文
在Flask中處理請求時(shí),應(yīng)用會(huì)生成一個(gè)“請求上下文”對象。整個(gè)請求的處理過程,都會(huì)在這個(gè)上下文對象中進(jìn)行。這保證了請求的處理過程不被干擾。處理請求的具體代碼如下:
- def wsgi_app(self, environ, start_response):
- with self.request_context(environ):
- # with語句中生成一個(gè)`response`對象
- ...
- return response(environ, start_response)
在Flask 0.9版本之前,應(yīng)用只有“請求上下文”對象,它包含了和請求處理相關(guān)的信息。同時(shí)Flask還根據(jù)werkzeug.local模塊中實(shí)現(xiàn)的一種數(shù)據(jù)結(jié)構(gòu)LocalStack用來存儲(chǔ)“請求上下文”對象。這在《一個(gè)Flask應(yīng)用運(yùn)行過程剖析 》中有所介紹。在0.9版本中,F(xiàn)lask又引入了“應(yīng)用上下文”的概念。本文主要Flask中的這兩個(gè)“上下文”對象。
LocalStack
在介紹“請求上下文”和“應(yīng)用上下文”之前,我們對LocalStack簡要做一個(gè)回顧。在Werkzeug庫——local模塊一文中,我們講解了werkzeug.local模塊中實(shí)現(xiàn)的三個(gè)類Local、LocalStack和LocalProxy。關(guān)于它們的概念和詳細(xì)介紹,可以查看上面的文章。這里,我們用一個(gè)例子來說明Flask中使用的一種數(shù)據(jù)結(jié)構(gòu)LocalStack。
- >>> from werkzeug.local import LocalStack
- >>> import threading
- # 創(chuàng)建一個(gè)`LocalStack`對象
- >>> local_stack = LocalStack()
- # 查看local_stack中存儲(chǔ)的信息
- >>> local_stack._local.__storage__
- {}
- # 定義一個(gè)函數(shù),這個(gè)函數(shù)可以向`LocalStack`中添加數(shù)據(jù)
- >>> def worker(i):
- local_stack.push(i)
- # 使用3個(gè)線程運(yùn)行函數(shù)`worker`
- >>> for i in range(3):
- t = threading.Thread(target=worker, args=(i,))
- t.start()
- # 再次查看local_stack中存儲(chǔ)的信息
- >>> local_stack._local.__storage__
- {<greenlet.greenlet at 0x4bee5a0>: {'stack': [2]},
- <greenlet.greenlet at 0x4bee638>: {'stack': [1]},
- <greenlet.greenlet at 0x4bee6d0>: {'stack': [0]}
- }
由上面的例子可以看出,存儲(chǔ)在LocalStack中的信息以字典的形式存在:鍵為線程/協(xié)程的標(biāo)識(shí)數(shù)值,值也是字典形式。每當(dāng)有一個(gè)線程/協(xié)程上要將一個(gè)對象push進(jìn)LocalStack棧中,會(huì)形成如上一個(gè)“鍵-值”對。這樣的一種結(jié)構(gòu)很好地實(shí)現(xiàn)了線程/協(xié)程的隔離,每個(gè)線程/協(xié)程都會(huì)根據(jù)自己線程/協(xié)程的標(biāo)識(shí)數(shù)值確定存儲(chǔ)在棧結(jié)構(gòu)中的值。
LocalStack還實(shí)現(xiàn)了push、pop、top等方法。其中top方法永遠(yuǎn)指向棧頂?shù)脑亍m數(shù)脑厥侵府?dāng)前線程/協(xié)程中***被推入棧中的元素,即local_stack._local.stack[-1](注意,是stack鍵對應(yīng)的對象中***被推入的元素)。
請求上下文
Flask中所有的請求處理都在“請求上下文”中進(jìn)行,在它設(shè)計(jì)之初便就有這個(gè)概念。由于0.9版本代碼比較復(fù)雜,這里還是以0.1版本的代碼為例進(jìn)行說明。本質(zhì)上這兩個(gè)版本的“請求上下文”的運(yùn)行原理沒有變化,只是新版本增加了一些功能,這點(diǎn)在后面再進(jìn)行解釋。
請求上下文——0.1版本
- # Flask v0.1
- 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):
- self.app = app
- self.url_adapter = app.url_map.bind_to_environ(environ)
- self.request = app.request_class(environ)
- self.session = app.open_session(self.request)
- self.g = _RequestGlobals()
- self.flashes = None
- def __enter__(self):
- _request_ctx_stack.push(self)
- def __exit__(self, exc_type, exc_value, tb):
- # do not pop the request stack if we are in debug mode and an
- # exception happened. This will allow the debugger to still
- # access the request object in the interactive shell.
- if tb is None or not self.app.debug:
- _request_ctx_stack.pop()
由上面“請求上下文”的實(shí)現(xiàn)可知:
- “請求上下文”是一個(gè)上下文對象,實(shí)現(xiàn)了__enter__和__exit__方法。可以使用with語句構(gòu)造一個(gè)上下文環(huán)境。
- 進(jìn)入上下文環(huán)境時(shí),_request_ctx_stack這個(gè)棧中會(huì)推入一個(gè)_RequestContext對象。這個(gè)棧結(jié)構(gòu)就是上面講的LocalStack棧。
- 推入棧中的_RequestContext對象有一些屬性,包含了請求的的所有相關(guān)信息。例如app、request、session、g、flashes。還有一個(gè)url_adapter,這個(gè)對象可以進(jìn)行URL匹配。
- 在with語句構(gòu)造的上下文環(huán)境中可以進(jìn)行請求處理。當(dāng)退出上下文環(huán)境時(shí),_request_ctx_stack這個(gè)棧會(huì)銷毀剛才存儲(chǔ)的上下文對象。
- 以上的運(yùn)行邏輯使得請求的處理始終在一個(gè)上下文環(huán)境中,這保證了請求處理過程不被干擾,而且請求上下文對象保存在LocalStack棧中,也很好地實(shí)現(xiàn)了線程/協(xié)程的隔離。
以下是一個(gè)簡單的例子:
- # example - Flask v0.1
- >>> from flask import Flask, _request_ctx_stack
- >>> import threading
- >>> app = Flask(__name__)
- # 先觀察_request_ctx_stack中包含的信息
- >>> _request_ctx_stack._local.__storage__
- {}
- # 創(chuàng)建一個(gè)函數(shù),用于向棧中推入請求上下文
- # 本例中不使用`with`語句
- >>> def worker():
- # 使用應(yīng)用的test_request_context()方法創(chuàng)建請求上下文
- request_context = app.test_request_context()
- _request_ctx_stack.push(request_context)
- # 創(chuàng)建3個(gè)進(jìn)程分別執(zhí)行worker方法
- >>> for i in range(3):
- t = threading.Thread(target=worker)
- t.start()
- # 再觀察_request_ctx_stack中包含的信息
- >>> _request_ctx_stack._local.__storage__
- {<greenlet.greenlet at 0x5e45df0>: {'stack': [<flask._RequestContext at 0x710c668>]},
- <greenlet.greenlet at 0x5e45e88>: {'stack': [<flask._RequestContext at 0x7107f28>]},
- <greenlet.greenlet at 0x5e45f20>: {'stack': [<flask._RequestContext at 0x71077f0>]}
- }
上面的結(jié)果顯示:_request_ctx_stack中為每一個(gè)線程創(chuàng)建了一個(gè)“鍵-值”對,每一“鍵-值”對中包含一個(gè)請求上下文對象。如果使用with語句,在離開上下文環(huán)境時(shí)棧中銷毀存儲(chǔ)的上下文對象信息。
請求上下文——0.9版本
在0.9版本中,F(xiàn)lask引入了“應(yīng)用上下文”的概念,這對“請求上下文”的實(shí)現(xiàn)有一定的改變。這個(gè)版本的“請求上下文”也是一個(gè)上下文對象。在使用with語句進(jìn)入上下文環(huán)境后,_request_ctx_stack會(huì)存儲(chǔ)這個(gè)上下文對象。不過與0.1版本相比,有以下幾點(diǎn)改變:
- 請求上下文實(shí)現(xiàn)了push、pop方法,這使得對于請求上下文的操作更加的靈活;
- 伴隨著請求上下文對象的生成并存儲(chǔ)在棧結(jié)構(gòu)中,F(xiàn)lask還會(huì)生成一個(gè)“應(yīng)用上下文”對象,而且“應(yīng)用上下文”對象也會(huì)存儲(chǔ)在另一個(gè)棧結(jié)構(gòu)中去。這是兩個(gè)版本***的不同。
我們先看一下0.9版本相關(guān)的代碼:
- # Flask v0.9
- def push(self):
- """Binds the request context to the current context."""
- top = _request_ctx_stack.top
- if top is not None and top.preserved:
- top.pop()
- # 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()
我們注意到,0.9版本的“請求上下文”的pop方法中,當(dāng)要將一個(gè)“請求上下文”推入_request_ctx_stack棧中的時(shí)候,會(huì)先檢查另一個(gè)棧_app_ctx_stack的棧頂是否存在“應(yīng)用上下文”對象或者棧頂?shù)?ldquo;應(yīng)用上下文”對象的應(yīng)用是否是當(dāng)前應(yīng)用。如果不存在或者不是當(dāng)前對象,F(xiàn)lask會(huì)自動(dòng)先生成一個(gè)“應(yīng)用上下文”對象,并將其推入_app_ctx_stack中。
我們再看離開上下文時(shí)的相關(guān)代碼:
- # Flask v0.9
- def pop(self, exc=None):
- """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.
- .. versionchanged:: 0.9
- Added the `exc` argument.
- """
- app_ctx = self._implicit_app_ctx_stack.pop()
- clear_request = False
- if not self._implicit_app_ctx_stack:
- self.preserved = False
- if exc is None:
- exc = sys.exc_info()[1]
- self.app.do_teardown_request(exc)
- clear_request = True
- rv = _request_ctx_stack.pop()
- assert rv is self, 'Popped wrong request context. (%r instead of %r)'
- % (rv, self)
- # 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)
上面代碼中的細(xì)節(jié)先不討論。注意到當(dāng)要離開以上“請求上下文”環(huán)境的時(shí)候,F(xiàn)lask會(huì)先將“請求上下文”對象從_request_ctx_stack棧中銷毀,之后會(huì)根據(jù)實(shí)際的情況確定銷毀“應(yīng)用上下文”對象。
以下還是以一個(gè)簡單的例子進(jìn)行說明:
- # example - Flask v0.9
- >>> from flask import Flask, _request_ctx_stack, _app_ctx_stack
- >>> app = Flask(__name__)
- # 先檢查兩個(gè)棧的內(nèi)容
- >>> _request_ctx_stack._local.__storage__
- {}
- >>> _app_ctx_stack._local.__storage__
- {}
- # 生成一個(gè)請求上下文對象
- >>> request_context = app.test_request_context()
- >>> request_context.push()
- # 請求上下文推入棧后,再次查看兩個(gè)棧的內(nèi)容
- >>> _request_ctx_stack._local.__storage__
- {<greenlet.greenlet at 0x6eb32a8>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
- >>> _app_ctx_stack._local.__storage__
- {<greenlet.greenlet at 0x6eb32a8>: {'stack': [<flask.ctx.AppContext at 0x5c96a58>]}}
- >>> request_context.pop()
- # 銷毀請求上下文時(shí),再次查看兩個(gè)棧的內(nèi)容
- >>> _request_ctx_stack._local.__storage__
- {}
- >>> _app_ctx_stack._local.__storage__
- {}
應(yīng)用上下文
上部分中簡單介紹了“應(yīng)用上下文”和“請求上下文”的關(guān)系。那什么是“應(yīng)用上下文”呢?我們先看一下它的類:
- class AppContext(object):
- """The application context binds an application object implicitly
- to the current thread or greenlet, similar to how the
- :class:`RequestContext` binds request information. The application
- context is also implicitly created if a request context is created
- but the application is not on top of the individual application
- context.
- """
- def __init__(self, app):
- self.app = app
- self.url_adapter = app.create_url_adapter(None)
- # Like request context, app contexts can be pushed multiple times
- # but there a basic "refcount" is enough to track them.
- self._refcnt = 0
- def push(self):
- """Binds the app context to the current context."""
- self._refcnt += 1
- _app_ctx_stack.push(self)
- def pop(self, exc=None):
- """Pops the app context."""
- self._refcnt -= 1
- if self._refcnt <= 0:
- if exc is None:
- exc = sys.exc_info()[1]
- self.app.do_teardown_appcontext(exc)
- rv = _app_ctx_stack.pop()
- assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
- % (rv, self)
- def __enter__(self):
由以上代碼可以看出:“應(yīng)用上下文”也是一個(gè)上下文對象,可以使用with語句構(gòu)造一個(gè)上下文環(huán)境,它也實(shí)現(xiàn)了push、pop等方法。“應(yīng)用上下文”的構(gòu)造函數(shù)也和“請求上下文”類似,都有app、url_adapter等屬性。“應(yīng)用上下文”存在的一個(gè)主要功能就是確定請求所在的應(yīng)用。
然而,以上的論述卻又讓人產(chǎn)生這樣的疑問:既然“請求上下文”中也包含app等和當(dāng)前應(yīng)用相關(guān)的信息,那么只要調(diào)用_request_ctx_stack.top.app或者魔法current_app就可以確定請求所在的應(yīng)用了,那為什么還需要“應(yīng)用上下文”對象呢?對于單應(yīng)用單請求來說,使用“請求上下文”確實(shí)就可以了。然而,F(xiàn)lask的設(shè)計(jì)理念之一就是多應(yīng)用的支持。當(dāng)在一個(gè)應(yīng)用的請求上下文環(huán)境中,需要嵌套處理另一個(gè)應(yīng)用的相關(guān)操作時(shí),“請求上下文”顯然就不能很好地解決問題了。如何讓請求找到“正確”的應(yīng)用呢?我們可能會(huì)想到,可以再增加一個(gè)請求上下文環(huán)境,并將其推入_request_ctx_stack棧中。由于兩個(gè)上下文環(huán)境的運(yùn)行是獨(dú)立的,不會(huì)相互干擾,所以通過調(diào)用_request_ctx_stack.top.app或者魔法current_app也可以獲得當(dāng)前上下文環(huán)境正在處理哪個(gè)應(yīng)用。這種辦法在一定程度上可行,但是如果對于第二個(gè)應(yīng)用的處理不涉及到相關(guān)請求,那也就無從談起“請求上下文”。
為了應(yīng)對這個(gè)問題,F(xiàn)lask中將應(yīng)用相關(guān)的信息單獨(dú)拿出來,形成一個(gè)“應(yīng)用上下文”對象。這個(gè)對象可以和“請求上下文”一起使用,也可以單獨(dú)拿出來使用。不過有一點(diǎn)需要注意的是:在創(chuàng)建“請求上下文”時(shí)一定要?jiǎng)?chuàng)建一個(gè)“應(yīng)用上下文”對象。有了“應(yīng)用上下文”對象,便可以很容易地確定當(dāng)前處理哪個(gè)應(yīng)用,這就是魔法current_app。在0.1版本中,current_app是對_request_ctx_stack.top.app的引用,而在0.9版本中current_app是對_app_ctx_stack.top.app的引用。
下面以一個(gè)多應(yīng)用的例子進(jìn)行說明:
- # example - Flask v0.9
- >>> from flask import Flask, _request_ctx_stack, _app_ctx_stack
- # 創(chuàng)建兩個(gè)Flask應(yīng)用
- >>> app = Flask(__name__)
- >>> app2 = Flask(__name__)
- # 先查看兩個(gè)棧中的內(nèi)容
- >>> _request_ctx_stack._local.__storage__
- {}
- >>> _app_ctx_stack._local.__storage__
- {}
- # 構(gòu)建一個(gè)app的請求上下文環(huán)境,在這個(gè)環(huán)境中運(yùn)行app2的相關(guān)操作
- >>> with app.test_request_context():
- print "Enter app's Request Context:"
- print _request_ctx_stack._local.__storage__
- print _app_ctx_stack._local.__storage__
- with app2.app_context():
- print "Enter app2's App Context:"
- print _request_ctx_stack._local.__storage__
- print _app_ctx_stack._local.__storage__
- # do something
- print "Exit app2's App Context:"
- print _request_ctx_stack._local.__storage__
- print _app_ctx_stack._local.__storage__
- # Result
- Enter app's Request Context:
- {<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
- {<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>]}}
- Enter app2's App Context:
- {<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
- {<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>, <flask.ctx.AppContext object at 0x0000000007313198>]}}
- Exit app2's App Context
- {<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<RequestContext 'http://localhost/' [GET] of __main__>]}}
- {<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>]}}
在以上的例子中:
- 我們首先創(chuàng)建了兩個(gè)Flask應(yīng)用app和app2;
- 接著我們構(gòu)建了一個(gè)app的請求上下文環(huán)境。當(dāng)進(jìn)入這個(gè)環(huán)境中時(shí),這時(shí)查看兩個(gè)棧的內(nèi)容,發(fā)現(xiàn)兩個(gè)棧中已經(jīng)有了當(dāng)前請求的請求上下文對象和應(yīng)用上下文對象。并且棧頂?shù)脑囟际莂pp的請求上下文和應(yīng)用上下文;
- 之后,我們再在這個(gè)環(huán)境中嵌套app2的應(yīng)用上下文。當(dāng)進(jìn)入app2的應(yīng)用上下文環(huán)境時(shí),兩個(gè)上下文環(huán)境便隔離開來,此時(shí)再查看兩個(gè)棧的內(nèi)容,發(fā)現(xiàn)_app_ctx_stack中推入了app2的應(yīng)用上下文對象,并且棧頂指向它。這時(shí)在app2的應(yīng)用上下文環(huán)境中,current_app便會(huì)一直指向app2;
- 當(dāng)離開app2的應(yīng)用上下文環(huán)境,_app_ctx_stack棧便會(huì)銷毀app2的應(yīng)用上下文對象。這時(shí)查看兩個(gè)棧的內(nèi)容,發(fā)現(xiàn)兩個(gè)棧中只有app的請求的請求上下文對象和應(yīng)用上下文對象。
- ***,離開app的請求上下文環(huán)境后,兩個(gè)棧便會(huì)銷毀app的請求的請求上下文對象和應(yīng)用上下文對象,棧為空。
與上下文對象有關(guān)的“全局變量”
在Flask中,為了更加方便地處理一些變量,特地提出了“全局變量”的概念。這些全局變量有:
- # Flask v0.9
- _request_ctx_stack = LocalStack()
- _app_ctx_stack = LocalStack()
- current_app = LocalProxy(_find_app)
- request = LocalProxy(partial(_lookup_object, 'request'))
- session = LocalProxy(partial(_lookup_object, 'session'))
- g = LocalProxy(partial(_lookup_object, 'g'))
- # 輔助函數(shù)
- def _lookup_object(name):
- top = _request_ctx_stack.top
- if top is None:
- raise RuntimeError('working outside of request context')
- return getattr(top, name)
- def _find_app():
- top = _app_ctx_stack.top
- if top is None:
- raise RuntimeError('working outside of application context')
- return top.app
可以看出,F(xiàn)lask中使用的一些“全局變量”,包括current_app、request、session、g等都來自于上下文對象。其中current_app一直指向_app_ctx_stack棧頂?shù)?ldquo;應(yīng)用上下文”對象,是對當(dāng)前應(yīng)用的引用。而request、session、g等一直指向_request_ctx_stack棧頂?shù)?ldquo;請求上下文”對象,分別引用請求上下文的request、session和g。不過,從 Flask 0.10 起,對象 g 存儲(chǔ)在應(yīng)用上下文中而不再是請求上下文中。
另外一個(gè)問題,在形成這些“全局變量”的時(shí)候,使用了werkzeug.local模塊的LocalProxy類。之所以要用該類,主要是為了動(dòng)態(tài)地實(shí)現(xiàn)對棧頂元素的引用。如果不使用這個(gè)類,在生成上述“全局變量”的時(shí)候,它們因?yàn)橹赶驐m斣兀鴹m斣卮藭r(shí)為None,所以這些變量也會(huì)被設(shè)置為None常量。后續(xù)即使有上下文對象被推入棧中,相應(yīng)的“全局變量”也不會(huì)發(fā)生改變。為了動(dòng)態(tài)地實(shí)現(xiàn)對棧頂元素的引用,這里必須使用werkzeug.local模塊的LocalProxy類。