一. flask中的经典错误
我们现在工程中新建test/test1.py:
from flask import Flask, current_app
app = Flask(__name__)
a = current_app # 如果调试, 这里会出现unbund未绑定
d = current_app.config['DEBUG']
运行test/test1.py,出现如下报错,: RuntimeError: Working outside of application context.
1.以前使用flask的request也出现过类似错误
2.我们在app/spider/yushu_book.py中使用过` current_app.config['PER_PAGE']`,为什么没有报错?
我们查看current_app的源码:
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
可以看到current_app, request, session, g都使用了LocalProxy, 这也是产生错误的原因所在。
二. AppContext、RequestContext、 Flask与Request之间的关系
AppContext应用上下文,其实是对Flask以及Flask外部一些其他内容的封装。
RequestCont请求上下文,同样的是对Request以及Request外部内容的封装
我们编码时,真正要使用的是Flask或Request对象,
但使用时必须从AppContext,RequestContext两个上下文中获取。
而LocalProxy代理,提供了访问Flask和Request的能力
三. 详解flask上下文与出入栈
flask运行的流程图:
1.请求进入时, 检测_app_ctx_stack栈中有没有app, 没有的话push推入
2.Request会被push推入_request_ctx_stack栈中
3.使用flask.current_app时,会从_app_ctx_stack.top得到app对象
4.使用flask.request时, 会从_request_ctx_stack.top得到Request对象
回过头来再看一次flask.current的源代码:
# context locals
_request_ctx_stack = LocalStack() # 存放Request的栈
_app_ctx_stack = LocalStack() # 存放app的栈
current_app = LocalProxy(_find_app) # 栈中取出app
request = LocalProxy(partial(_lookup_req_object, 'request')) # 栈中取出Request
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
其中current_app = LocalProxy(_find_app)
的_find_app, 就是从栈中取出app:
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
request = LocalProxy(partial(_lookup_req_object, 'request'))
的_lookup_req_object,就是栈中取出request:
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(@app.route做装饰器的函数中), 不会报错, 因为有请求进入的之后, flask会自动确保栈中有app。
而test1.py中, 我们就得手动推入,才不会报错。 修改test/test1.py:
from flask import Flask, current_app
app = Flask(__name__)
ctx = app.app_context() # 获取封装了app的context上下文
ctx.push() # push入栈中
a = current_app
d = current_app.config['DEBUG']
ctx.pop() # 结束记得pop出栈
我们往往在离线应用和单元测试中, 会手动push和pop
四. flask上下文与with语句
test/test1.py可以使用上下文管理器with改写:
with app.app_context():
a = current_app
d = current_app.config['DEBUG']
效果是一样的。 这里涉及with上下文管理器的原理:
1. 对实现了上下文协议的对象,使用with语句
2. 对实现了上下文协议的对象称为 上下文管理器
3. 有__enter__ __exit__即实现了上下文协议
我们阅读app_context()的源码:
def app_context(self):
"""
···
"""
return AppContext(self)
AppContext()的源码:
class AppContext(object):
"""
···
"""
def __init__(self, app):···
def push(self):···
def pop(self, exc=_sentinel):···
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb): # __exit__ 会除self外,再传3个参数
self.pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
首先 app.app_context()函数返回了AppContent, 而AppContent这个类,有__enter__和__exit__方法, 并在其中进行了push和pop。
另外with的注意点:
使应with .... as XXXX: 时,这里的 XXXX是__enter__的return对象
自定义一段代码
class A:
def __enter__(self):
a = 1
def __exit__(self):
b = 2
with A() as obj_A:
pass
进行调试过程中, 发现obj_A 为None。
对自定义代码修改:
class A:
def __enter__(self):
a = 1
return a
def __exit__(self):
b = 2
with A() as obj_A:
pass
这回进行调试时, 发现obj_A是a.
但不会进入__exit__, 并且在运行结束时, 自定义代码会报错!
五. 详解上下文管理器的__exit__方法
上面自定义代码运行出错的原因,其实是__exit__传入的参数不对。__exit__除self外必须传入三个参数exc_type, exc_value, tb
修改代码:
class MyResource:
def __enter__(self):
print('connect to resource')
return self
def __exit__(self, exc_type, exc_value, tb):
print('close resource connection')
b = 2
def query(self):
print('query data')
with MyResource() as resource:
resource.query()
调试发现可以进入__exit__并且, 运行完后,不会报错.
那么问题来了,exc_type, exc_value, tb
这三个最为必需品的参数又是干什么的呢?
这三个参数其实是做异常处理的, 修改代码:
class MyResource:
def __enter__(self):
print('connect to resource')
return self
def __exit__(self, exc_type, exc_value, tb):
print('close resource connection')
b = 2
def query(self):
print('query data')
with MyResource() as resource:
1/0 # 故意出错
resource.query()
在__exit__中print('close resource connection')
这行打上断点, 可以看到这三参数:
exc_type {type}<class 'ZeroDivision' > 异常类型
exc_value division by zero 异常原因描述
tb traceback Object at 0x1113f06c8 异常堆栈
所以我们可以在__exit__中, 通过查看tb参数来判断是否出现异常, 例子:
class MyResource:
def __enter__(self):
print('connect to resource')
return self
def __exit__(self, exc_type, exc_value, tb):
if tb: # tb有值, 表明出现异常
print('process execption')
else:
print('no exception')
print('close resource connection')
def query(self):
print('query data')
with MyResource() as resource:
1/0
resource.query()
对__exit__的返回值补充说明:
如果__exit__返回了True, 则__exit__顺利退出, 不会向外抛异常。
如果__exit__返回False或无return, 则__exit__结束后会向外抛出异常。