vlambda博客
学习文章列表

Flask源码之请求上下文和应用上下文

我们继续来看wsgi_app这个方法,我去掉了一些无关代码,那些部分会在其他博文中介绍

def wsgi_app(self, environ, start_response): # 调用了self.request_context这个方法 # 此方法把environ封装成了RequestContext对象,这个对象看名字也知道这个是请求上下文 ctx = self.request_context(environ) 
def request_context(self, environ): # 注意request_context是Flask的类方法,那么self就是Flask的实例或者说对象 # 也就是说下面的 RequestContext 的实例化需要Flask实例和environ参数 return RequestContext(self, environ)

简单看一下RequestContext的定义,位置在源码的ctx.py

class RequestContext(object): def __init__(self, app, environ, request=None, session=None): self.app = app if request is None: request = app.request_class(environ) self.request = request

我们看到, 第一个参数 app接受的是 Flask实例

回到wsgi_app这个方法,我们看到接下来ctx.push这一句调用了RequestContextpush方法,这个方法就是把自身也就是 RequestContext实例压入到 LocalStack这个数据结构中

 def wsgi_app(self, environ, start_response): # ctx是 RequestContext 实例 ctx = self.request_context(environ) error = None try: try: # 只看这一句 ctx.push() response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) except: # noqa: B001 error = sys.exc_info()[1] raise return response(environ, start_response) finally: if self.should_ignore_error(error): error = None ctx.auto_pop(error)

Local和LocalStack

flask中实现了LocalLocalStack两种数据结构,Local的作用是线程隔离和协程隔离,类似threadlocal,可以看成下面的字典。RequestContext暂时可以认为存储在这样的字典中

{12345:RequestContext}

12345是线程id,RequestContext是请求上下文,之所以用Local做是为了在wsgi server是多线程或多协程的情况下,每个线程或协程获取自己的上下文,不会相互干扰。毕竟我们知道多线程是共享内存中的对象的

但实际上,flask是把RequestContext存储在栈(后进先出)结构中,准确地说是LocalStack结构中,类似下面的字典

{12345:{'stack':[RequestContext]}}

这样做是因为一个请求可能有多个请求上下文,比如服务端重定向的情况(一个请求要去A视图,我们需要重定向到B视图,这就需要自己再push一个RequestContext

此外还有多个应用上下文的情况

入栈

有了对LocalLocalStack的说明,我们回到 wsgi_app方法中,继续看 push方法,看是否确实是把 RequestContext压入到LocalStack

 def wsgi_app(self, environ, start_response): # ctx是 RequestContext 实例 ctx = self.request_context(environ) error = None try: try: # 只看这一句 ctx.push() response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) except: # noqa: B001 error = sys.exc_info()[1] raise return response(environ, start_response) finally: if self.should_ignore_error(error): error = None ctx.auto_pop(error)

ctrl+鼠标左键点击push

 def push(self): top = _request_ctx_stack.top

_request_ctx_stack的定义再globals.py

_request_ctx_stack = LocalStack()

它现在还是的状态是这样,栈是空的

{12345:'stack':[]}

如果栈是空的话,我们把self也就是RequestContext压入栈,在最后一句,注意pushRequestContext的方法,所以self就是RequestContext的实例

 def push(self): top = _request_ctx_stack.top
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)

我们还注意到这里还有一个_app_ctx_stack,这也是LocalStack,位置在globals.py

_app_ctx_stack = LocalStack()

只不过这个栈里面存储的是应用上下文,类似下面的字典

{12345:'stack':[AppContext]}

然后我们也执行了app_ctx.push()方法,也就是把应用上下文压入栈

到这里我们就知道了,执行RequestContext对象的push方法会把RequestContext的实例压入_request_ctx_stack中,还会把AppContext的实例压入_app_ctx_stack

如何获取请求上下文

我们常常需要在一个视图函数中获取请求中的参数,例如urlremote_address

我们当然可以每次手动获取_request_ctx_stack栈顶的RequestContext对象,然后调用RequestContextrequest属性,但每次操作栈结构还是有点繁琐,像下面这样

from flask import Flask, request, Requestfrom flask.globals import _request_ctx_stack
flask_app = Flask(__name__)

@flask_app.route('/')def hello_world(): req: Request = _request_ctx_stack.top.request remote_addr=req.remote_addr return "{}".format(remote_addr)

flask的做法是使用LocalProxy

LocalProxy

from flask import Flask, request

request就是LocalProxy,它是代理模式在flask中的运用,作用就是代理对RequestContext.request的操作

我们执行request.remote_addr就相当于执行 _request_ctx_stack.top.request.remote_addr

那为什么用LocalProxy而不是直接request=_request_ctx_stack.top.request呢?

原因是这样写,项目run的时候,这句 request=_request_ctx_stack.top.request 就已经执行了,但是项目启动的时候_request_ctx_stack.top还是None,因为还没有请求进来,push方法还没执行。这就导致了request固定成了None,这显然不行

我们看看LocalProxy是怎么做的,我先介绍原理,再介绍实现细节

原理

原理就是 request是一个代理对象,代理了对 RequestContext.request的操作

具体来说:

我们 重写了__getattr__方法,让每次执行 request.remote_addr会先去 LocalStack中拿到 RequestContext,然后执行 RequestContext.request.remote_addr,获取其他属性也是一样

也就是说,代理模式延迟了 被代理对象的获取,代理对象Localproxy创建的时候不会获取,获取被代理对象属性的时候才会获取被代理对象

实现

#LocalProxy传入了一个偏函数,偏函数的作用是获取被代理对象request = LocalProxy(partial(_lookup_req_object, "request"))
# _lookup_req_object("request")=partial(_lookup_req_object, "request")()=RequestContext.requestdef _lookup_req_object(name): top = _request_ctx_stack.top if top is None: raise RuntimeError(_request_ctx_err_msg) return getattr(top, name)
class LocalProxy(object): __slots__ = ("__local", "__dict__", "__name__", "__wrapped__") def __init__(self, local, name=None):  object.__setattr__(self, "_LocalProxy__local", local)

def _get_current_object(self): if not hasattr(self.__local, "__release_local__"): return self.__local()
def __getattr__(self, name): return getattr(self._get_current_object(), name)

再看LocalProxy

1.__getattr__request.remote_addr会执行 __getattr__ 这个方法,相当于__getattr__("remote_addr") 这个方法又会去调用_get_current_object获取被代理的对象,也就是 RequestContext.request,之后调用 getattr(self._get_current_object(), name)获取 RequestContext.request的指定属性,例如 remote_addr

总结

到这里,我们就可以看出flask中的请求上下文是如何存储和获取的

请求上下文存储在LocalStack结构中,Local是为了线程安全,LocalStack是为了多请求上下文的场景

而获取是通过LocalProxy,使用代理模式是为了动态地获取请求上下文,在访问request属性的时候,才会从栈中获取真实的请求上下文,然后代理 属性的获取

flask中还有应用上下文,curren_app,我们有时候会通过current_app.config来获取配置信息,原理和request类似,只不过代理的是AppContext对象

说了这么多,wsgi_app的第一步,也就是请求的第一步,就是先把请求上下文和应用上下文入栈