上下文
在使用 Flask 开发项目时,经常会使用到 4 个全局变量,即 request、session、current_app 和 g,这 4 个全局变量就是上下文对象。上下文对象是 Flask 中的一个非常优雅的设计,其作用是在一个请求到来之后,不需要再把一些常用的对象在函数间层层传递。Flask 中的上下文对象相关说明如表 7-2 所示。

线程隔离对象
在学习上下文对象的原理之前,我们首先需要理解线程隔离对象。线程隔离对象可以使数据在多个线程间拥有独自的备份,不会被其他线程影响。在 Flask 中,每收到一个请求则开启一个线程,而表 7-2 中的上下文对象因为是被存放到了线程隔离对象中,所以即使是定义成全局变量,在每个线程间都独有一份备份。
在 Python 内置的 threading 模块中,通过 threading.local() 即可创建一个用于保存线程隔离对象的变量,示例代码如下。
import threading
thread_local = threading.local()
thread_local.name = "我是主线程的"
def thread_func(index):
thread_local.name = f"我是{index}线程的"
print(thread_local.name)
if __name__ == '__main__':
for x in range(1, 3):
th = threading.Thread(target=thread_func, kwargs={"index": x})
th.start()
th.join()
print(thread_local.name)
上述代码中,首先创建了 threading.local 类的对象,然后在这个对象上绑定了 name 属性,之后在主线程和子线程中分别赋不同的值。执行上述代码会发现,每个线程的name值都是不一样的,并且一个线程修改了 name 的值,并不会影响到其他线程。线程隔离对象实现的原理并不复杂,我们只需根据线程id进行区分即可。
除了内置的 threading 模块提供的线程隔离对象外,werkzeug 也单独定义了一个 werkzeug.local.Local 类,这个类的实现逻辑与 threading.local 大同小异,下面我们先来看 werkzeug.local.Local 源代码(为了让读者关注核心代码,笔者对源代码进行了删改)。

上述代码中,首先在 Local 类中创建 _storage
对象,其值为 ContextVar 对象,又在 ContextVar 中则定义了一个 storage 对象,此对象为一个字典类型,如果当前环境中安装了 greenlet,则使用 greenlet 的协程 id 作为字典的键,否则使用 threading 模块的线程 id 作为键。storage 字典的值也是一个字典,这个字典中存放的就是绑定到这个 Local 对象上的属性名和属性值。阅读 Local 的 __getattr__
和 __setattr__
方法后发现,绑定到 Local 对象上的属性,实际上是间接绑定到 _storage.storge
上了。werkzeug.local.Local
就是通过这种技术,实现了线程隔离的对象。
LocalStack类
werkzeug.local.LocalStack 类是一个将对象存放到 werkzeug.local.Local 上的栈结构。flask.request、flask.app 等都是存放在这个类的对象上的,以下为 LocalStack 的源代码(为了便于理解,笔者对源代码进行了删改)。

上述代码提供了非常方便的 push 方法,用于往栈中添加数据,通过 pop 方法从栈顶中删除数据,通过 top 属性可以获取栈顶数据。并且所有数据都存放在 Local 对象上,因此保证了数据在多线程中的独有性。那么 LocalStack 在哪里用到了呢?我们进入 flask.globals 模块中,可以看到以下两行代码。
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
其中 _request_ctx_stack 用来存放请求上下文的栈对象,_app_ctx_stack 用来存放应用上下文的栈对象。那么又在哪里使用到了这两个对象呢?在一个请求到达 Flask 项目之后,首先会执行 flask.app.wsgi_app 方法,此方法相关源代码如下。

接收到请求后,wsgi_app 首先会通过 self.request_context 创建一个 RequestContext(请求上下文)对象 ctx,然后调用 ctx.push 方法,我们再来看 ctx.push 方法的源代码。
def push(self) -> None:
# 获取请求上下文栈顶元素
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)
# 将请求上下文推到栈顶
_request_ctx_stack.push(self)
...
查看 push 方法代码后,我们看到了 _request_ctx_stack 和 _app_ctx_stack 两个对象。整体的逻辑是先将应用上下文推送到 _app_ctx_stack 栈顶,然后再推送请求上下文到 _request_ctx_stack 栈顶。这一步操作使我们明白了,请求上下文和应用上下文是一起创建并一起销毁的,但是我们会有如下几个疑问。
-
这里推送到请求上下文和应用上下文的对象是后面用到的 request 和 current_app 吗?
-
LocalStack 中的 LocalProxy 又是用来做什么的?为什么需要它?
LocalProxy类
通过以下代码可以看到,current_app、request、session、g 这 4 个上下文对象全部是 werkzeug.local.LocalProxy 对象。
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
request: "Request" = LocalProxy(partial(_lookup_req_object, "request"))
session: "SessionMixin" = LocalProxy( # type: ignore
partial(_lookup_req_object, "session")
)
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g"))
那么 LocalProxy 是怎么实现的呢?我们先来看 LocalProxy 的核心源代码。

因为 LocalProxy 的源代码非常多,我们只截取其中的核心部分。LocalProxy 是一个代理对象,会对创建 LocalProxy 时传进来的 local 对象进行代理。在使用 LocalProxy 对象的某个属性时,会自动执行 _get_current_object 方法,此方法会判断 self.__local
是否可以调用,如果可以调用,则执行调用,否则从 self.__local
上获取 self.__name
指定的值。我们再回过头看 current_app
和 request
的实现,代码如下。
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
current_app: "Flask" = LocalProxy(_find_app)
request: "Request" = LocalProxy(partial(_lookup_req_object, "request"))
以上代码中,current_app 是将 _find_app 函数传给了 LocalProxy,而 _find_app 做的事情非常简单,就是从 _app_ctx_stack 应用上下文上获取栈顶数据。所以在使用 current_app 时,实际上是先执行 LocalProxy._get_current_object 方法,然后再执行 _find_app 方法将 _app_ctx_stack 栈顶数据进行返回。
再看 request,其在创建 LocalProxy 时,传入的是一个偏函数,这里用偏函数可以把 request 传入 _lookup_req_object 进行调用。执行此函数时,首先获取 _request_ctx_stack 的栈顶元素,然后再获取栈顶元素上的 request 属性。
有读者可能会有疑惑,为什么表7-2 中的 4 个上下文对象需要使用 LocalProxy 进行代理?原因是 Flask 中的上下文都是动态推送和删除的,如果不用代理,表7-2 中的 4 个上下文对象只会被赋值一次,不会随着栈元素的更新而更新。