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 = appif request is None:request = app.request_class(environ)self.request = request
我们看到, 第一个参数 app接受的是 Flask实例
回到wsgi_app这个方法,我们看到接下来ctx.push这一句调用了RequestContext的push方法,这个方法就是把自身也就是 RequestContext实例压入到 LocalStack这个数据结构中
def wsgi_app(self, environ, start_response):# ctx是 RequestContext 实例ctx = self.request_context(environ)error = Nonetry:try:# 只看这一句ctx.push()response = self.full_dispatch_request()except Exception as e:error = eresponse = self.handle_exception(e)except: # noqa: B001error = sys.exc_info()[1]raisereturn response(environ, start_response)finally:if self.should_ignore_error(error):error = Nonectx.auto_pop(error)
Local和LocalStack
flask中实现了Local和LocalStack两种数据结构,Local的作用是线程隔离和协程隔离,类似threadlocal,可以看成下面的字典。RequestContext暂时可以认为存储在这样的字典中
{12345:RequestContext}
12345是线程id,RequestContext是请求上下文,之所以用Local做是为了在wsgi server是多线程或多协程的情况下,每个线程或协程获取自己的上下文,不会相互干扰。毕竟我们知道多线程是共享内存中的对象的
但实际上,flask是把RequestContext存储在栈(后进先出)结构中,准确地说是LocalStack结构中,类似下面的字典
{12345:{'stack':[RequestContext]}}
这样做是因为一个请求可能有多个请求上下文,比如服务端重定向的情况(一个请求要去A视图,我们需要重定向到B视图,这就需要自己再push一个RequestContext)
此外还有多个应用上下文的情况
入栈
有了对Local和LocalStack的说明,我们回到 wsgi_app方法中,继续看 push方法,看是否确实是把 RequestContext压入到LocalStack中
def wsgi_app(self, environ, start_response):# ctx是 RequestContext 实例ctx = self.request_context(environ)error = Nonetry:try:# 只看这一句ctx.push()response = self.full_dispatch_request()except Exception as e:error = eresponse = self.handle_exception(e)except: # noqa: B001error = sys.exc_info()[1]raisereturn response(environ, start_response)finally:if self.should_ignore_error(error):error = Nonectx.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压入栈,在最后一句,注意push是RequestContext的方法,所以self就是RequestContext的实例
def push(self):top = _request_ctx_stack.topapp_ctx = _app_ctx_stack.topif 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中
如何获取请求上下文
我们常常需要在一个视图函数中获取请求中的参数,例如url,remote_address
我们当然可以每次手动获取_request_ctx_stack栈顶的RequestContext对象,然后调用RequestContext的request属性,但每次操作栈结构还是有点繁琐,像下面这样
from flask import Flask, request, Requestfrom flask.globals import _request_ctx_stackflask_app = Flask(__name__)@flask_app.route('/')def hello_world():req: Request = _request_ctx_stack.top.requestremote_addr=req.remote_addrreturn "{}".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.topif 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的第一步,也就是请求的第一步,就是先把请求上下文和应用上下文入栈
