写文章其实很费力,你的「在看」很重要。
前言
在面试 Python web 方面的工作时,如果你说自己阅读过 Flask 源码,那么 Flask 上下文机制的实现原理肯定会被问到,本篇文章就来剖析一下 Flask 上下文机制。
Flask 上下文作用
什么是上下文?
日常生活中的上下文:从一篇文章中抽取一段话,你阅读后,可能依旧无法理解这段话中想表达的内容,因为它引用了文章其他部分的观点,要理解这段话,需要先阅读理解这些观点。这些散落于文章的观点就是这段话的上下文。
程序中的上下文:一个函数通常涉及了外部变量 (或方法),要正常使用这个函数,就需要先赋值给这些外部变量,这些外部变量值的集合就称为上下文,自行琢磨一下。
Flask 的视图函数需要知道前端请求的 url、参数以及数据库等应用信息才可以正常运行,要怎么做?
一个粗暴的方法是将这些信息通过传参的方式一层层传到到视图函数,太不优雅。Flask 为此设计出了自己的上下文机制,当需要使用请求信息时,直接 fromflaskimportrequest
就可以获得当前请求的所有信息并且在多线程环境下是线程安全的,很酷。
实现这种效果的大致原理其实与 threading.local 实现原理相同,创建一个全局的字典对象,利用线程 id 作为 key,相应的数据作为 value,这样,不同的线程就可以获取专属于自己的数据。
Flask 上下文机制
Flask 上下文定义在 globals.py 上,代码如下。
# flask/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
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
# partial()构建一个偏函数
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))
Flask 中看似有多个上下文,但其实都衍生于 _request_ctx_stack
与 _app_ctx_stack
, _request_ctx_stack
是请求上下文, _app_ctx_stack
是应用上下文。
常用的 request 和 session 衍生于 _request_ctx_stack
,current_app 和 g 衍生于 _app_ctx_stack
。
可以发现,这些上下文的实现都使用了 LocalStack 和 LocalProxy,这两个类的实现在 werkzeug 中,在实现这两个类之前,需要先理解 Local 类,代码如下。
# werkzeug/local.py
class Local(object):
__slots__ = ("__storage__", "__ident_func__")
def __init__(self):
# 调用__setattr__()方法设置值。
object.__setattr__(self, "__storage__", {})
# 获得线程id
object.__setattr__(self, "__ident_func__", get_ident)
# 迭代不同线程对应的字典对象
def __iter__(self):
return iter(self.__storage__.items())
# 返回 LocalProxy 对象
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
# pop() 清除当前线程保存的数据
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)
Local 类的代码很好理解, __init__()
方法创建了 __storage__
用于存储数据与 __ident_func__
用于获得线程 id,这里使用 object.__setattr__()
来实现赋值,这样做是不是有什么深意?
没有,只是一个 ticks。
因为 Local 类本身重写了 __setattr__()
方法,如果直接使用 self.__storage__={}
进行赋值,就会调用重写的 __setattr__
方法,导致报错,所以赋值要使用父类 object 的 __setattr__
。object.__setattr__('name','二两')
与 object.name='二两'
效果完成一样,不用觉得太高深。
Local 类重写了 __getattr__
、 __setattr__
和 __delattr__
,从而自定义了 Local 对象属性访问、设置与删除对应的操作,这些方法都通过 __ident_func__()
方法获取当前线程 id 并以此作为 key 去操作当前线程对应的数据,Local 通过这种方式实现了多线程数据的隔离。
值得一提, __ident_func__()
也可以获得协程的 id,但需要安装 greenlet 库,本文以线程为主讨论对象。
LocalStack 类是基于 Local 类实现的栈结构,代码如下。
# werkzeug/local.py
# 构建一个栈
class LocalStack(object):
def __init__(self):
# 实例化 Local类
self._local = Local()
# 清除当前线程保存的数据
def __release_local__(self):
self._local.__release_local__()
@property
def __ident_func__(self): # 获得当前线程id
return self._local.__ident_func__
@__ident_func__.setter
def __ident_func__(self, value):
object.__setattr__(self._local, "__ident_func__", value)
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):
# 利用list来构建一个栈
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()调用的依旧是__release_local__()
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 类的代码简洁易懂,主要的逻辑就实例化 Local 类,获得 local 对象,在 local 对象中添加 stack,以 list 的形式来实现一个栈,至此可知, _request_ctx_stack
与 _app_ctx_stack
这两个上下文就是一个线程安装的栈,线程所有的信息都会保存到相应的栈里,直到需要使用时,再出栈获取。
LocalProxy 类是 Local 类的代理对象,它的作用就是将操作都转发到 Local 对象上。
# werkzeug/local.py
@implements_bool
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__"):
object.__setattr__(self, "__wrapped__", local)
def _get_current_object(self):
if not hasattr(self.__local, "__release_local__"):
return self.__local() # 获得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())
# 获得local对象中对应name的值
return getattr(self._get_current_object(), name)
def __setitem__(self, key, value):
# 为local对象赋值
self._get_current_object()[key] = value
def __delitem__(self, key):
del self._get_current_object()[key]
__setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
# ... 省略部分代码
LocalProxy 类在 __init__()
方法中将 local 实例赋值给 _LocalProxy__local
并在后续的方法中通过 __local
的方式去操作它,这其实也是一个 ticks。
因为 LocalProxy 类重写了 __setattr__
方法,所以不能直接复制,此时要通过 object.__setattr__()
进行赋值。根据 Python 文档可知 (参考小节会给出出处 url),任何形式上以双下划线开头的私有变量 __xxx
,在文本上均会替换成 _classname__xxx
,而 __setattr__
会直接操作文本,所以给 _LocalProxy__local
赋值。
LocalProxy 类后面的逻辑其实都是一层代理,将真正的处理交个 local 对象。
结尾
考虑到字数,上下文的内容拆成 2 篇,下篇会提出几个问题并给出相应的回答与看法。
1.Python 中有 thread.local 了,werkzeug 为什么还要自己弄一个 Local 类来存储数据?2. 为什么不直接使用 Local?而要通过 LocalStack 类将其封装成栈的操作?3. 为什么不直接使用 Local?而要通过 LocalProxy 类来代理操作?
反正我看源码时会有这样的疑惑,这其实也是设计的精髓,是我们要学习的地方,卖个关子,下篇见。
如果这篇文章对你有所启发,点个「在看」支持二两。
参考:
flask 源码解析:上下文
What is the meaning of a single and a double underscore before an object name?
Python 文档:Private Variables