写文章其实很费力,你的「在看」很重要。
前言
在面试Python web方面的工作时,如果你说自己阅读过Flask源码,那么Flask上下文机制的实现原理肯定会被问到,本篇文章就来剖析一下Flask上下文机制。
Flask上下文作用
什么是上下文?
日常生活中的上下文:从一篇文章中抽取一段话,你阅读后,可能依旧无法理解这段话中想表达的内容,因为它引用了文章其他部分的观点,要理解这段话,需要先阅读理解这些观点。这些散落于文章的观点就是这段话的上下文。
程序中的上下文:一个函数通常涉及了外部变量(或方法),要正常使用这个函数,就需要先赋值给这些外部变量,这些外部变量值的集合就称为上下文,自行琢磨一下。
Flask的视图函数需要知道前端请求的url、参数以及数据库等应用信息才可以正常运行,要怎么做?
一个粗暴的方法是将这些信息通过传参的方式一层层传到到视图函数,太不优雅。Flask为此设计出了自己的上下文机制,当需要使用请求信息时,直接from flask import request
就可以获得当前请求的所有信息并且在多线程环境下是线程安全的,很酷。
实现这种效果的大致原理其实与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