上下文

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

image 2025 01 21 17 34 01 083
Figure 1. 表7-2 Flask中的上下文对象

线程隔离对象

在学习上下文对象的原理之前,我们首先需要理解线程隔离对象。线程隔离对象可以使数据在多个线程间拥有独自的备份,不会被其他线程影响。在 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 源代码(为了让读者关注核心代码,笔者对源代码进行了删改)。

image 2025 01 21 17 36 21 686

上述代码中,首先在 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 的源代码(为了便于理解,笔者对源代码进行了删改)。

image 2025 01 21 17 38 19 785

上述代码提供了非常方便的 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 方法,此方法相关源代码如下。

image 2025 01 21 17 39 43 666

接收到请求后,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 的核心源代码。

image 2025 01 21 17 44 35 213

因为 LocalProxy 的源代码非常多,我们只截取其中的核心部分。LocalProxy 是一个代理对象,会对创建 LocalProxy 时传进来的 local 对象进行代理。在使用 LocalProxy 对象的某个属性时,会自动执行 _get_current_object 方法,此方法会判断 self.__local 是否可以调用,如果可以调用,则执行调用,否则从 self.__local 上获取 self.__name 指定的值。我们再回过头看 current_apprequest 的实现,代码如下。

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 个上下文对象只会被赋值一次,不会随着栈元素的更新而更新。