Flask 源码分析-上下文
本文主要分析了 Flask 的上下文机制,包括:application context
和 request context
。
Local 的实现
在 Flask 中,视图函数处理 http 请求时,需要知道具体的请求信息,比如URL、请求方法、参数等,以及应用信息。Flask 将这些信息封装成上下文对象,类似于全局变量,不同的是:它们是动态的,每个线程都有自己独立的上下文信息,不会相互干扰。
那么是如何实现这种机制的?在 Python 多线程编程中,有个 threading.local
类,可以实现访问某个变量时只有该线程自己能看到,实现的原理是,这个对象保存一个字典,以线程 ID 为键,读取该对象时,它动态地查询当前线程 ID 对应的数据。Flask 使用 Local
类实现了类似的效果,下面是它的实现代码。
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
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)
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}
在 Local
类的构造函数中,初始化了两个属性,一个是 __storage__
,这个属性是一个字典,用来保存上下文数据;另一个属性是 __ident_func__
,用来获取当前线程的 ID。从 __getattr__
方法可以看出,我们在通过点号运算获取数据时,根据当前线程 ID: self.__ident_func__()
,访问嵌套字典里定义的 name-value
键值对,从而获取保存的上下文数据。存储数据的方法在 __setattr__
中定义,将数据保存在 storage[ident][name]
字典中,外层键为线程 ID。通过 Local
类,可以实现数据的访问隔离。
另外,__call__
操作会创建一个 LocalProxy
对象,LocalProxy
会在下面讲到。
LocalStack 的实现
Flask 中,Local
是怎样与上下文关联的呢,这就得研究LocalStack
和 LocalProxy
的实现。首先我们来看 LocalStack
的定义。
class LocalStack(object):
def __init__(self):
self._local = Local()
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()
LocalStack
的属性 _local
是一个 Local
对象,在 push
方法中,将数据保存在 self._local.stack
,即属性Local
的 __storage__[ident][stack]
里。实际上 LocalStack
实现了线程隔离的栈访问,它提供了操作栈结构的 push
,pop
,top
方法。
LocalProxy 的实现
LocalProxy
是 Local
对象的代理,负责把所有对自己的操作转发给内部的 Local
对象。
class LocalProxy(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):
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__)
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
构造函数中初始化的 _LocalProxy__local
属性,后面可以通过 self.__local
来访问。_get_current_object
方法用来获取当前线程对应的对象,当 __local
值为 Local
对象时,_get_current_object
返回的是__storage__[self.__ident_func__()][name]
。
上下文的定义
接下来研究上下文在 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'))
request
的值是将 _request_ctx_stack
中的栈顶元素弹出,根据 Local.[self.__ident_func__()]['stack'][-1].request
获取。那么请求上下文是在什么时候入栈的呢?
在 wsgi_app
方法中,将对应的请求信息入栈,每次请求过来的时候,就会调用该代码。
ctx = self.request_context(environ)
ctx.push()
具体是怎样做的,让我们继续查看内部实现。
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.session = None
self._implicit_app_ctx_stack = []
self.match_request()
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)
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
RequestContext
保存了当前请求的信息,比如 request
对象和 app
对象。在前面的代码
中,就是获取了当前请求上下文的 request
对象
request = LocalProxy(partial(_lookup_req_object, 'request'))
push
方法把该请求的对应的 AppContext
和 RequestContext
分别压入到 _app_ctx_stack
和 _request_ctx_stack
。
到这里,上下文的实现就比较清晰了:每次有请求过来的时候,flask 会先创建当前线程或者进程需要处理的两个上下文对象,把它们保存到隔离的栈里面,这样视图函数进行处理的时候就能直接从栈上获取这些信息。