Flask的上下文
flask 中有两种上下文:application context 和 request context。上下文有关的内容定义在 globals.py 文件
# 关键处,后面会解释
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
# 请求上下文栈对象
_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'))
这里的实现用到了两个东西:LocalStack 和 LocalProxy。它们两个的结果就是咱们能够动态地获取两个上下文的内容,在并发程序中每一个视图函数都会看到属于本身的上下文,而不会出现混乱。
LocalStack 和 LocalProxy 都是 werkzeug 提供的,定义在 local.py 文件中,分析以前,先了解一个相似于threading.local 的效果的类Local,它实现了多线程或者多协程状况下全局变量的隔离效果
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__ 中,产生了{'__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__':{
'线程或者协程的 id':{name:value}
}}
`
了解了Local类后,回来分析LocalStack 和 LocalProxy
LocalStack 是基于 Local 实现的栈结构。若是说 Local 提供了多线程或者多协程隔离的属性访问,那么 LocalStack 就提供了隔离的栈访问
class LocalStack(object):
def __init__(self):
# 实例化Local类,并封装到LocalStack类中
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):
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):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
咱们能够看到最上面有一行代码,他就是实例化一个LocalStack对象:
# 实例化 请求上下文栈对象
_request_ctx_stack = LocalStack()
# 它会把当前线程或者协程的请求都保存在栈里,等使用的时候再从里面读取
LocalProxy 是一个 Local 对象的代理,负责把全部对本身的操做转发给内部的 Local 对象。
class LocalProxy(object):
__slots__ = ('__local', '__dict__', '__name__')
def __init__(self, local, name=None):
# 产生了{'__local':local对象}结构,为何_LocalProxy__local会变成__local,请参考面向对象的私有属性
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
def _get_current_object(self):
"""用于获取当前线程或者协程对应的对象"""
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
初步了解了LocalStack和LocalProxy类后,还要了解一个小知识点:
偏函数:
import functools
# 偏函数
def func(a1,a2):
print(a1,a2)
# 帮func函数传递参数,并产生新的函数名
new_func = functools.partial(func,123)
# 新的函数只须要传一个参数
new_func(2)
如今再看下面的代码
_request_ctx_stack = LocalStack()
# 调用 _lookup_req_object方法,拿到封装在 请求上下文 里的request对象
request = LocalProxy(partial(_lookup_req_object, 'request'))
# 调用 _lookup_req_object方法,拿到封装在 请求上下文 里的session对象
session = LocalProxy(partial(_lookup_req_object, 'session'))
若是你在疑惑 :何时请求上下文里封装了request对象和session对象?那你可记得以前的wsgi_app方法:
def wsgi_app(self, environ, start_response):
# 先建立RequestContext对象,并封装了request对象
ctx = self.request_context(environ)
error = None
try:
try:
# ctx.push的内部完成了什么?
# 1. 先创建了_request_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
# 完成功能代码: RequestContext/push方法里的 top = _request_ctx_stack.top
# 2. 再建立_app_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
# 完成功能代码: RequestContext/push方法里的 app_ctx = _app_ctx_stack.top
# 3. 再建立AppContext对象并封装了app和g
# 完成功能代码: RequestContext/push方法里的 app_ctx = self.app.app_context()
# 4. 把AppContext对象压入到栈中,从而造成了{"__storage__":{线程ID:{'stack':[app_ctx对象]}}}数据结构
# 完成功能代码: app_ctx.push()
# 5. 把RequestContext对象压入到栈中,从而造成了{"__storage__":{线程ID:{'stack':[ctx对象]}}}数据结构
# 完成功能代码: _request_ctx_stack.push(self)
# 6. RequestContext对象封装session
# 完成功能代码: self.session = session_interface.open_session(self.app, self.request)
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
ctx.push()的详细分析:
# RequestContext类中push方法
def push(self):
#1. 先创建了_request_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
top = _request_ctx_stack.top
# 2. 再建立_app_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
# 3. 再建立AppContext对象并封装了app和g
app_ctx = self.app.app_context()
# 4. 把AppContext对象压入到栈中,从而造成了{"__storage__":{线程ID:{'stack':[app_ctx对象]}}}数据结构
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
# 5. 把RequestContext对象压入到栈中,从而造成了{"__storage__":{线程ID:{'stack':[ctx对象]}}}数据结构
_request_ctx_stack.push(self)
if self.session is None:
session_interface = self.app.session_interface
# 6. RequestContext对象封装session
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
if self.url_adapter is not None:
# 实现了路由的匹配逻辑
self.match_request()
首先从下面两行代码开始:
ctx = self.request_context(environ)
ctx.push()
每次在调用 app.__call__
的时候,都会把对应的请求信息压栈,最后执行完请求的处理以后把它出栈。
咱们来看看request_context, 这个 方法只return一个类:
def request_context(self, environ):
# 调用了 RequestContext,并把 self 和请求信息的字典 environ 当作参数传递进去
return RequestContext(self, environ)
class RequestContext(object):
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):
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):
"""把该请求的 请求上下文和应用上下文 有关的信息保存到各自对应的栈上,具体看上面"""
top = _request_ctx_stack.top
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):
""" 和push相反,把 请求上下文和应用上下文 有关的信息从各自对应的栈上删除"""
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()
if clear_request:
rv.request.environ['werkzeug.request'] = None
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 会先建立当前线程或者进程须要处理的两个重要上下文对象,把它们保存到隔离的栈里面,这样视图函数进行处理的时候就能直接从栈上获取这些信息
`
上面分析事后,你可能有三个疑惑:
# 1. 不会产生多个app吗?
`不会,此处app对象运用了单例模式,因此只有一个app对象,所以多个 request 共享了 application context`
# 2. 为何要把 请求上下文 和 应用上下文 分开?每一个请求不是都同时拥有这两个上下文信息吗?
`由于'灵活性',虽然在实际运行中,每一个请求对应一个 请求上下文 和一个 应用上下文,可是在测试或者 python shell 中运行的时候,用户能够单首创建 请求上下文 或者 应用上下文,这种灵活度方便用户的不一样的使用场景;`
# 3. 为何 请求上下文 和 应用上下文 都有实现成栈的结构?每一个请求难道会出现多个 请求上下文 或者 应用上下文 吗?
`在web runtime 时,栈永远只有1个对象。可是在写离线脚本时,才会用在栈中放多个对象.(建立一个py文件本地运行)`
第三个问题的代码示例:
# --------------------------例1------------------------------------
from flask import current_app,g
from pro_excel import create_app
app1 = create_app()
with app1.app_context(): # AppContext对象(app,g) -> local对象
print(current_app.config) # -1 top app1
app2 = create_app()
with app2.app_context(): # AppContext对象(app,g) -> local对象
print(current_app.config) # top -1 app2
print(current_app.config) # top -1 app1
# 写离线脚本且多个上下文嵌套时,才会在栈中添加多个对象。
# ---------------------------例2-----------------------------------
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。