在web程序运行时,可能会有许多请求,这些请求中包含了许多信息,比如url、参数、路由、请求方式等等。 这些信息在视图函数中可能会被用到,它们就是上下文。那么如何保存这些上下文,到了需要的时候再调用呢?因为这些请求是动态的,flask需要动态地访问它们。可能我说的这些不太好理解,下面看例子:
from flask import Flask
from flask import request,current_app
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
@app.route('/')
def index():
url=request.url
config = current_app.config
return "hello world"
@app.route('/sample',methods=['GET','POST'])
def sample():
url = request.url
app.config['SECRET_KEY'] = 'very important key'
config = current_app.config
return "test"
app.run()
开启调试模式运行上面这段代码时,我分别访问了本地的https://127.0.0.1:5000/和https://127.0.0.1:5000/sample,调试过程部分值如下。
从上面的两张图片我们可以清楚地看到,因为请求不同,所以request和current_app也有所变化。但是我们一开始就从flask里面导入了request和current_app,那么flask是怎么做到动态改变这两个上下文的值的呢?(注:flask有两种上下文,一个是application context,一种是request context。这两种上下文又分别被分为两种。application context被分为current app和g,request context被分为request和session,这四个值都是全局变量。)
先来看一部分源码。这部分源码在app.py中,这两个函数都是类Flask方法。一旦有请求出现,call函数被调用,然后执行wsgi_app()函数,在wsgi_app函数中,有这么一句: ctx = self.request_context(environ)。循迹追踪下去,request_context函数返回了一个RequestContext实例。根据后面的ctx.push()可以猜测,在push函数中,RequestContext对象将两种上下文压栈。至于RequestContext怎么实现的后面再详细讲。
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
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)
def request_context(self, environ):
return RequestContext(self, environ)
那么,它们是怎么被调用的呢?这就要分析werkzeug的源码了,因为flask上下文这部分的实现和werkzeug联系很紧密。
这部分实现,我们从werkzeug中的Local类开始分析。我们看到,Local类有两个属性,storage是一个列表,列表中的每一个元素都是字典。字典的键值是当前线程协程的id,字典的值则又是一个字典,这个子字的键值,在下面我们会看到,会有两种,一种是request,一种是session,值就是当前线程或协程的上下文。Local类用getattr、setattr以及delattr分别完成了对列表元素的访问、添加、删除。
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
def __iter__(self):
return iter(self.__storage__.items())
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)
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)
但是这里还仅仅是做到了用线程id作为唯一标识,组成id:{‘request’:req1}这样的键值对,并保存到列表里,并没有实现栈以及动态访问。werkzeug提供了两个类,进一步实现了这个功能。如下所示,这两个类分别是LocalStack和LocalProxy。LocalStack这个类,将Local类的实例作为自己的属性,实现了请求上下文的入栈出栈、栈顶元素访问。(push函数第二参数obj就是请求上下文)。LocalProxy则是把Local类封装了一层,实现了动态访问请求上下文。
class LocalStack(object):
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
def _get__ident_func__(self):
return self._local.__ident_func__
def _set__ident_func__(self, value):
object.__setattr__(self._local, '__ident_func__', value)
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
del _get__ident_func__, _set__ident_func__
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup)
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
class LocalProxy(object):
__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
def __init__(self, local, name=None):
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
if callable(local) and not hasattr(local, '__release_local__'):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, '__wrapped__', local)
def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
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 __dir__(self):
try:
return dir(self._get_current_object())
except RuntimeError:
return []
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
def __delitem__(self, key):
del self._get_current_object()[key]
分析到了这里,也没看见这这两个类到底是怎样实现动态获取上下文的,别急。看看下面的代码。下面的代码是flask框架globals.py部分的代码,这部分代码被设置为了全局的。在这里面我们看到current_app、request、session、g四个上下文。你会不会想起,有时候我们会用到这样的代码:from flask import current_app,request,现在我们知道它们是从哪里来的了。但是问题似乎还没有解决,至少目前看来我们还是不知道flask是怎么做的。
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'))
还记得前面的ResponseContext类吗?在前面给出的代码ctx = self.request_context(environ) ctx.push()
我们推测,正是在这里,上下文进栈。实际上request_context函数返回的是ResonseContext实例。在下面的代码中我们可以看到push函数。在push函数中获取了_app_ctx_stack的栈顶元素(application context),如果栈顶元素为空或者栈顶元素中的app不是当前app,就把当前的application context入栈。然后再把request context入栈。注意,这里入栈的两个push , _request_ctx_stack.push(self) 的self是ResponseContext类, app_ctx.push() 入栈的是ApplicationContext类。
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.flashes = None
self.session = None
self._implicit_app_ctx_stack = []
self.preserved = False
self._preserved_exc = None
self._after_request_functions = []
self.match_request()
def _get_g(self):
return _app_ctx_stack.top.g
def _set_g(self, value):
_app_ctx_stack.top.g = value
g = property(_get_g, _set_g)
del _get_g, _set_g
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
if top is not None and top.preserved:
top.pop(top._preserved_exc)
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)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_request_ctx_stack.push(self)
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(
self.app, self.request
)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
def pop(self, exc=_sentinel):
app_ctx = self._implicit_app_ctx_stack.pop()
try:
clear_request = False
if not self._implicit_app_ctx_stack:
self.preserved = False
self._preserved_exc = None
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
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
# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)
assert rv is self, 'Popped wrong request context. ' \
'(%r instead of %r)' % (rv, self)
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)
至此脉络应该很清晰明了了,通过语句 from flask import current_app,request 在一开始就导入到程序中去的,current_app和request,只是一个被初始化的实例,这个时候,它们的栈是空栈。等到客户端向服务器发送请求,请求被转发到flask框架,然后入栈。在调用的时候如:url = request.url ,就会去取出栈顶元素,返回相应的值。这样一来,就可以做到在视图函数中动态处理请求。依靠的就是上下文。