这一篇我们将深入学习flask的上下文机制。
1. Working outside of application context引发的思考
我们抒写一段测试代码,进行debug调试。
我们实例化了一个Flask核心对象,从current_app
读取的却是一个本地代理LocalProxy
。当尝试从current_app
中读取数据的时候就会触发RuntimeError: Working outside of application context
。
我们从图中看到current_app
并不是指向flask的核心对象。
我们看下current_app
的源码:
我们看到current_app
和request
都是LocalProxy
类。这引发了一系列的思考:
什么是LocalProxy
,current_app
和核心对象的关系,request
是Request
吗?
让我们带着疑问进行下个小节学习吧ヾ(◍°∇°◍)ノ゙
2. AppContext、RequestContext、Flask和Request之间的关系
在flask中有两个上下文,一个是应用上下文,一个是请求上下文。我们把这两个上下文当做对象来思考。这两个对象就是对flask和request对象的一些封装。比如说应用上下文是对核心对象Flask
对象的封装,请求上下文是对Request
对象的封装,然后在此基础上封装一些好用的方法。
我们看下关于AppContext
和RequestContext
的源码:
我们在实例化方法中看到将flask核心对象存储到了AppContext
的属性中,并且实现了左侧红框四个方法。
我们在实例化方法中看到将Request
对象存储到了RequestContext
的属性中(右侧红线框),并且实现了左侧红框四个方法。
flask核心对象
Flask
存储在AppContext
中,Request
对象存储在RequestContext
中。
我们对这四个对象的作用和意义做下解释:
Flask
是flask的核心对象,里面保存了配置信息,提供了注册路由和视图函数的一系列功能。AppContext
是对Flask
进行了封装,并增加了一些额外的参数。Request
保存了一些请求信息,如:请求参数和完整的url等。RequestContext
是对Request
进行了封装,并增加了一些额外的参数。
我们在编码的时候真正想使用的是Flask
和Request
,但是并不是直接导入,我们应该从AppContext
和RequestContext
获得这两个对象。
如何获得呢?这就是我们接下来要学习的LocalProxy
,通过它提供的间接操作上下文的能力,直接获得想要的对象。
看下源码:
1 | current_app = LocalProxy(_find_app) |
上面的current_app
和request
分别指向Flask
和Request
,通过设计模式中的本地代理模式来从上下文中获得。
3. 详解flask上下文与出入栈
这一小节是flask核心知识点,学会了是面试装逼利器。我们看下老师总结的一张图片。
我们从一个web请求进入到flask框架来探讨下核心机制。
当一个请求进入到flask框架后,flask首先会实例化一个请求上下文RequestContext
,这个请求上下文封装了此次请求的一些信息(保存在Request中),在实例化完成之后会将上下文使用push
方法推到一个栈_request_ctx_stack
中。在flask中使用LocalStack
来表示栈,_request_ctx_stack
就是LocalStack
的实例化对象。
注意:在实例化RequestContext
对象推入到栈_request_ctx_stack
之前会先进行一次检查,检查栈_app_ctx_stack
的栈顶是否为空或者是否为当前对象,如果不是将实例化一个AppContext
对象,并将其推入到栈_app_ctx_stack
当中,然后再将RequestContext
的实例化对象推入到栈_request_ctx_stack
中,一定要注意这个逻辑。
我们再看下current_app
和request
这两个变量。
这两个变量永远指向对应的栈的栈顶。因此当我们使用这两个变量的时候实际上就是操作这两个栈顶对用的上下文。当栈顶对应的上下文存在的时候就能正常使用,当栈顶不存在上下文的时候就会报文章开头的Working outside of application context
和unbound
状态。
下面我们看看一开始的代码,想下如何解决unbound
。
我们知道之所以出现unbound
是因为栈顶为空,我们可以自己推上下文到栈中。
我们可以使用flask提供的app_context
方法获得上下文,我么看下源码。
1 | def app_context(self): |
我们从函数的文档注释可以看到我们所学习的知识点。
我们获得上下文之后手动推到栈顶,这样就能获得current_app
。
之前我们说过current_app
指向的是对应程序上下文的栈顶,但是current_app
这个代理具体返回的是什么呢?我们来看下源码:
1 | def _find_app(): |
我们看到current_app
代理最终返回的是flask的核心对象app
。
同样看下request
的源码:
1 | def _lookup_req_object(name): |
最终返回也不是请求上下文,而是request
对象。
完整的代码:
1 | from flask import Flask, current_app, request, Request |
对于单元测试和离线应用的时候,使用手动推栈很有必要。
4. flask上下文与with语句
我们之前看AppContext
源码的时候看到了实现了上下文协议的两个魔法方法__enter__
和__exit__
。因此我们可以使用with语句简化上面的代码。
1 | from flask import Flask, current_app, request, Request |
关于with语句的一些知识:
实现了上下文协议的对象才能使用with
实现了上下文协议的对象被称为上下文管理器
只要一个对象实现了
__enter__
__exit__
魔法方法就是实现了上下文协议
with后面的语句被称为上下文表达式,上下文表达式必须返回一个上下文管理器
上下文表达式必须返回一个上下文管理器,那下面的 as
后面的 f
代表什么呢?
1 | with open(r'') ad f: |
as
后面的变量是指魔法方法__enter__
的返回值
我们写段代码验证下:
我们看到的确是__enter__
函数的返回值,当什么都不返回的时候,将会为None。
5. 上下文管理器中的exit方法
我们看到__exit__
参数多了几个,分别是代表异常类型,异常值,和异常栈。
__exit__
也是有返回值的,分别是True
和False
。当返回True
的时候在with语句不再抛出异常,当返回False
的时候外面会继续抛出异常。如果我们在with外面用try…except捕捉,将继续捕捉到错误。当我们什么都不返回就是代表返回None,即False。