flask的上下文管理机制
接下来,从以下三个大的方面分别探讨flask的两大上下文管理机制。
- 方面一:请求进来时
- 方面二:视图函数
- 方面三:请求结束前
先来一个最简单的flask版的Hello World
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "Hello World" if __name__ == '__main__': app.run()
启动一个flask项目时,会先执行app.run()方法,这是整个项目的入口,执行run方法时,接着执行werkzeug模块中的run_simple
werkzeug中触发调用了Flask的__call__方法
1. 请求进来时
触发执行__call__方法,__call__方法的逻辑很简单,直接执行wsgi_app方法,将包含所有请求相关数据和一个响应函数传进去。
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return '<h1>Hello, web!</h1>' 上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数: environ:一个包含所有HTTP请求信息的dict对象; start_response:一个发送HTTP响应的函数。 在application()函数中,调用: start_response('200 OK', [('Content-Type', 'text/html')])
备注:__call__是一个符合wsgi标准的函数
执行wsgi_app方法
def wsgi_app(self, environ, start_response): 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: error = sys.exc_info()[1] raise return response(environ, start_response) finally: if self.should_ignore_error(error): error = None ctx.auto_pop(error)
第一步先执行了一个request_context的方法,将environ传进去,最后返回一个RequestContext类的对象,被ctx的变量接收(ctx=request_context(environ))
def request_context(self, environ): """Create a :class:`~flask.ctx.RequestContext` representing a WSGI environment. Use a ``with`` block to push the context, which will make :data:`request` point at this request. See :doc:`/reqcontext`. Typically you should not call this from your own code. A request context is automatically pushed by the :meth:`wsgi_app` when handling a request. Use :meth:`test_request_context` to create an environment and context instead of this method. :param environ: a WSGI environment """ return RequestContext(self, environ)
这个ctx对象在初始化时,赋了两个非常有用的属性,一个是request,一个是session
def __init__(self, app, environ, request=None): self.app = app if request is None: request = app.request_class(environ) self.request = request self.url_adapter = app.create_url_adapter(self.request) self.flashes = None self.session = None
这两个属性中request是一个Request()对象,这个对象就是我们在flask中使用的request对象,为我们提供了很多便捷的属性和方法,比如:request.method、request.form、request.args等等,另一个属性是session,初始为None。
紧接着执行ctx.push()方法,这个方法中,在执行请求上下文对象ctx之前先实例化了一个app_context对象,先执行了app_context的push方法,然后才执行_request_ctx_stack对象中的top和_request_ctx_stack.push(self),最后对ctx中的session进行处理。
所以,flask中的应用上下文发生在请求上下文之前。
def push(self): top = _request_ctx_stack.top if top is not None and top.preserved: top.pop(top._preserved_exc) # 在执行request_context请求上下文的push方法时,先执行了app_context应用上下文的push方法 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) if hasattr(sys, 'exc_clear'): sys.exc_clear() # 然后执行请求上下文对象中LocalStack对象的push方法 _request_ctx_stack.push(self) # 最后处理session if self.session is None: session_interface = self.app.session_interface self.session = session_interface.open_session( self.app, self.request ) if self.session is None: self.session = session_interface.make_null_session(self.app)
但是我们先说请求上下文,在处理完应用上下文的push方法后,紧接着执行了_request_ctx_stack对象的两个方法。
而这个_request_ctx_stack是LocalStack这个类的对象。_request_ctx_stack = LocalStack()
LocalStack有没有很眼熟,没错,flask内部使用的机制就是类似于我们上文中自定义的LocalStack的机制,实例化过程中使用了面向对象中的组合概念,self._local = Local(),然后在自身又实现了push、pop、top方法,这三个方法中都是通过反射从Local类的实例化对象中对一个stack属性进行append、pop、[-1]的操作,所以,Local对象中的stack属性对应的值一定是一个类似于列表的东西。通过对列表的操作,实现一个类似于栈的存取。
接着聊聊这个Local类,在实例化时,会对每个对象生成一个storage的空字典。我们翻遍整个Local类的源码,发现内部并没有实现一个叫stack的方法或者属性,但是上面我们提到了LocalStack对象会对Local对象中的一个叫stack的东西进行一系列操作。找不到不会报错吗?
这就是flask的巧妙之处,通过类的一些魔法方法巧妙的实现了相应的处理。在前引中,提到如果对象中没有某个属性,取值时,最终会执行类中的__getattr__方法,然后再做后续的异常处理,flask将所有的对应逻辑都实现在了类的__getattr__方法中,将每一个线程存储到字典中,在请求进来时,将每一个对应的请求ctx存在一个列表中,使用时直接调用,而不是通过传参的形式,更体现出了flask框架的轻量级。
处理完_request_ctx_stack后,就该处理session了。
在flask中,处理session时,非常的巧妙,完美的遵循了开闭原则,会先执行session_interface对象的open_session方法,在这个方法中,会先从用户请求的cookie中获取sessionid,获取该用户之前设置的session值,然后将值赋值到ctx.session中。
处理完session后,ctx.push方法就执行完了,返回到最开始的app.wsgi_app方法中,执行完push方法后,接着执行full_dispatch_request方法,从这个名字中我们也能猜到,这个方法只要是负责请求的分发。
def full_dispatch_request(self): self.try_trigger_before_first_request_functions() try: request_started.send(self) rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() except Exception as e: rv = self.handle_user_exception(e) return self.finalize_request(rv)
在full_dispatch_request方法中先执行preprocess_request方法,这个方法,会先执行所有被before_request装饰器装饰的函数,然后就通过路由的分发执行视图函数了(dispatch_request)
2.执行视图函数时
在执行视图函数之前,先执行了before_request,在执行我们的视图函数。
视图函数主要处理业务逻辑。在视图函数中可以调用request对象,进行取值,也可以调用session对象对session的存取。
在整个request的请求生命周期中,获取请求的数据直接调用request即可,对session进行操作直接调用session即可。request和session都是LocalProxy对象,借助偏函数的概念将对应的值传入_lookup_req_object函数。先从_request_ctx_stack(LocalStack)对象中获取ctx(请求上下文对象),再通过反射分别获取request和session属性。整个过程中LocalStack扮演了一个全局仓库的角色,请求进来将数据存取,需要时即去即用。所以,flask实现了在整个请求的生命周期中哪儿需要就直接调用的特色。
request = LocalProxy(partial(_lookup_req_object, 'request')) session = LocalProxy(partial(_lookup_req_object, 'session'))
3.请求结束前
视图函数执行完后,dispatch_request执行结束,执行full_dispatch_request方法的返回值finalize_request方法。这个方法中,同样的,在返回响应之前,先执行所有被after_request装饰器装饰的函数。
---->finalize_request ------> process_response
def process_response(self, response): ctx = _request_ctx_stack.top bp = ctx.request.blueprint funcs = ctx._after_request_functions if bp is not None and bp in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[bp])) if None in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) if not self.session_interface.is_null_session(ctx.session): self.session_interface.save_session(self, ctx.session, response) return response
执行process_response过程中,执行完after_request后,然后,执行session的save_session方法。将内存中保存在ctx.session的值取到后,json.dumps()序列化后,写入响应的cookie中(set_cookie),最后返回响应。
def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) # If the session is modified to be empty, remove the cookie. # If the session is empty, return without setting the cookie. if not session: if session.modified: response.delete_cookie( app.session_cookie_name, domain=domain, path=path ) return # Add a "Vary: Cookie" header if the session was accessed at all. if session.accessed: response.vary.add('Cookie') if not self.should_set_cookie(app, session): return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) samesite = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.get_signing_serializer(app).dumps(dict(session)) # set_cookie将session写入响应的cookie中 response.set_cookie( app.session_cookie_name, val, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure, samesite=samesite )
返回响应后,自动的调用ctx.auto_pop(error),将Local中存储的ctx对象pop掉,整个请求结束。
应用上下文
与请求上下文类似,当请求进来时,先实例化一个AppContext对象app_ctx,在实例化的过程中,提供了两个有用的属性,一个是app,一个是g。self.app就是传入的全局的app对象,self.g是一个全局的存储值的对象。接着将这个app_ctx存放到LocalStack()。
class AppContext(object): def __init__(self, app): self.app = app self.url_adapter = app.create_url_adapter(None) self.g = app.app_ctx_globals_class()
视图函数中,我们就可以调用app对象和g对象,如果我们使用蓝图构建我们的项目时,在每一个直接引用app就会造成循环引用的异常,这时,应用上下文就会显得非常有用,我们可以直接调用current_app就可以在整个生命周期中使用我们的app对象了。比如使用我们的配置项:current_app.config
current_app = LocalProxy(_find_app)
最后,当视图函数执行结束后,从storage中pop掉app_ctx对象。
from flask import Flask,request,session,g app = Flask(__name__) # type:Flask @app.before_request def auth_demo(): g.val = 123 @app.route('/') def index(): print(g.val) return "Hello World" if __name__ == '__main__': app.run()
注意问题:
1.flask上下文管理:
简单说 —
falsk上下文管理可以分为三个阶段:
1、请求进来时,将请求相关的数据放入上下问管理中;
2、在视图函数中,要去上下文管理中取值;
3、请求响应,要将上下文管理中的数据清除;
详细说 —
1、请求刚进来,将request,session封装在RequestContext类中,app,g封装在AppContext类中,并通过LocalStack将requestcontext和appcontext放入Local类中; 2、视图函数中,通过localproxy--->偏函数--->localstack--->local取值; 3、请求相应时,先执行save.session()再各自执行pop(),将local中的数据清除;
2.flask第三方组件
flask: -flask-session 默认放入cookie,可以放入redis -flask-redis -flask-migrate -flask-script -blinker 信号
公共: DBUtils 数据库连接池 wtforms 表单验证+生成HTML标签 sqlalchemy
自定义:Auth 参考falsk-login
3.Flask中的session是什么时候创建,什么时候销毁的?
当请求进来时,会将requset和session封装为一个RequestContext对象,通过LocalStack将RequestContext放入到Local对象中,因为 请求第一次来session是空值,所以执行open_session,给session(uuid4())赋值,再通过视图函数处理,请求响应时执行save.session,将签名session写入cookie中,再讲Local中的数值pop掉。
4.flask中一共有几个LocalStack和Local对象?
两个LocalStack,两个Local: request、session共同用一个LocalStack和Local; g、app共同用一个Localstack和Local;
5.为什么把请求放到RequestContext中?
因为request和session都是在视图中操作频繁的数据,也是用户请求需要用的数据,将request和session封装在RequestContext中top,pop一次就可以完成,而单独不封装在一起就会多次操作, ctx = RequestContext(request,session)
6.local作用?
-保存:请求上下文对象ctx和app上下文对象;
-做到“线程”间的数据隔离;
-localstack的源码与threading.local(线程处理)作用相似,不同之处是Local是通过greenlet(协程)获取唯一标识,粒度更细;
7.Localstack作用?
将local对象中的数据维护成一个栈[ctx,ctx](先进后出) { “协程或线程的唯一标识”: { stack:[ctx,ctx,ctx,] } }
8.为什么维护成一个栈?
8.1 当是web应用时:不管是单线程还是多线程,栈中只有一个数据 - 服务端单线程: { 111:{stack: [ctx, ]} } - 服务端多线程: { 111:{stack: [ctx, ]} 112:{stack: [ctx, ]} }
8.2 离线脚本:可以在栈中放入多个数据,多数多作为测试
with app01.app_context(): print(current_app) with app02.app_context(): print(current_app) print(current_app)
实际工作中,蓝图即可解决app分发的问题。
9.什么是g?
g 相当于一次请求的全局变量,当请求进来时将g和current_app封装为一个APPContext类,在通过LocalStack将Appcontext放入Local中,取值时通过偏函数,LocalStack、loca l中取值,响应时将local中的g数据删除;
10.怎么获取Session/g/current_app/request?
通过 、偏函数(lookup_req_object)、Localstack、Local取值
11.Flask的内置组件?
- 配置
- 路由
- 视图
- 模板
- session
- 蓝图
- 闪现
- 装饰器
- 中间件
12.flask第三方组件?
- flask-session;私有,将原来保存在Cookies中的session数据保存在redis或者memcache中;
- DBUtils;公共,数据库连接池,维护数据库连接;
- WTforms;公共,用于做表单验证,生成html标签;
本文出自:https://www.cnblogs.com/zhaopanpan/
十分感谢,flask请求上下文说的很透彻!