Inside Flask - globals 全局变量(对象代理)
框架是一个容器,在框架内编程,一般是要遵守框架的约定和使用模式。通常这样的模式是 IoC,即由框架调用用户的代码,而不是用户调用框架。框架需要记录当前的状态,并提供给用户代码使用。常用的 Jsp Servelet 、ASP.net 等,将请求和状态封装为向用户代码提供的 request 、session 等对象。在 flask 中,完成这些工作的是上下文 ctx (context) 和 globals 的全局对象。
flask/globals.py
文件中只使用了简单的 30 行左右的代码构建 flask 使用到的全局对象(更准确地说是对象代理)。虽说这些对象是全局可访问的,其实它们是线程隔离,即两个不同的请求,不同使用到相同的对象。Python 的强大动态语言特性,使得不同运行线程上的代码总是能正确地获取到该线程上对应的对象实例。
整个文件包括了几个对象 ::
_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 是对象代理,均来自 werkzeug 库。这两个类的设计原理是理解 flask 的 globals 对象的设计和安全性的关键,因此这个解析一下。
LocalStack
LocalStack 的底层是 werkzeug 里的 Local 类,它提供一个与线程绑定的字典,查看代码如下 ::
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
原理其实相当简单!
Local 中的 __storage__
是一个字典,__ident_func__
是线程(协程)的取 id 函数。get_ident
函数根据采用不同的运行方案而不同,如果使用多线程方式运行服务器,那么用 thread 模块里面的 get_ident
函数,如果是通过 greenlet 协程方式,那么用 from greenlet import getcurrent as get_ident
。
第二步,Local 的 __getattr__
、 __setattr__
和 __delattr__
3 个对属性操作的 magic 方法,在存取数据时,都通过 __storage__
和 __ident_func
进行 ::
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)
因此,每次得到的数据都会是本线程(协程)中的数据,A 线程上服务的用户绝不会拿到 B 线程上服务的用户数据(还涉及到上下文 ctx 的生命周期,没在此处描述)。
LocalStack 是对 Local 的简单包装,以支持以栈的方式读取数据,增加 push pop top 等栈方法。其实它无非是在 Local 加一个 stack 属性,代码为证 (_local
即为被包装的 Local 对象) ::
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
LocalProxy
LocalProxy 是一个对象代理类,即它会把调用传递到真实后端对象,一个不太恰当的例子如下 ::
a = LocalProxy(func_find_real_obj)
a.do_something() => b = func_find_real_obj() => b.do_something()
这个例子与 LocalProxy 不同的地方在于,例子中通过 func_find_real_obj
函数查找真实对象。而 LocalProxy 支持两种查找方法,一是在一个 Local 类对象容器中,找一个名字为 name 的对象;二是当 local 是一个函数时,直接从 local() 函数 取得被代理对象(让取得对象的过程更像是直接取一个变量,而不是调用函数,看起来更有幂等性)。
LocalProxy 的设计关键包括两个方面:(1)如何寻找到被代理对象;(2)如果把调用传递到被代理对象。在 werkzeug 中,LocalProxy 从 Local 类的对象中找被代理对象,然后通过 python 的 magic 方法传到到该对象。
LoalProxy 的 __init__
方法如下 ::
def __init__(self, local, name=None):
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
local 就是保存对象容器的地方,而 name 是所被代理对象的名字。
这里有个奇怪的地方 _LocalProxy__local
,它涉及到python 的私有变量的处理方法,即通过 _classname__spam
形式隐藏变量名,这里把它当作 __local
私有变量即可。
LocalProxy 通过 _get_current_object()
方法取得被代理对象 ::
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__) # 如果是从 Local 对象取
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)
看 return getattr(self.__local, self.__name__)
就是从一个 Local 类的对象中取得当前线程(协程)里的名字为 self.__name__
对象(第二种方法不需要这个名字)。
最后,LocalProxy 实现了一大堆的 magic 方法,去调用真实对象 ::
...
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
def __repr__(self):
try:
obj = self._get_current_object()
except RuntimeError:
return '<%s unbound>' % self.__class__.__name__
return repr(obj)
...
整合
OK,现在整合一下上面的两个类的设计原理,来看 globals 里面的对象 ::
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
_request_ctx_stack
和 _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'))
这几个都是通过查函数方式取得被代理的真实对象。
OK,问题来了,这里面的都是代理而已,那么真实的被代理对象是哪里来的?答案是它们是 flask 在处理请求的过程中由 flask 生成,然后保存下来的。
以 _request_ctx_stack
为例说明这个过程。
flask 处理请求时,按照 wsgi 规范, wsgi 框架调用 flask app 的 wsgi_app
函数,即 ::
web 请求 => wsgi 框架包装 =》 wsgi_app()
在 flask/app.py
中,这个函数前面两行代码,就生成了 _request_ctx_stack
::
def wsgi_app(self, environ, start_response):
...
ctx = self.request_context(environ)
ctx.push()
其它几个代理的真实对象也差不多是相同的处理流程,就不再讨论。