上一篇文章中,解决*RuntimeError: Working outside of application context.*错误,使用手动将应用上下文推入栈中:
ctx = app.app_context()
ctx.push()
print(current_app.name)
ctx.pop()
而 flask 文档中给我们的解决代码是:
with app.app_context():
print(current_app.name)
它使用了python的with语句,使得代码更加简洁。
AppContext 类
with语句的关键是需要实现__enter__和__exit__类方法,来看一下 flask 的AppContext类的实现:
class AppContext(object):
...
def push(self):
"""Binds the app context to the current context."""
...
def pop(self, exc=_sentinel):
"""Pops the app context."""
...
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
self.pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
AppContext类的__enter__方法实现了push()入栈,__exit__方法实现了pop()出栈。可以让我们不需要啰嗦的再去写入栈出栈语句。
with语句
怎样自己实现with语句呢?首先需要知道pep0343{:target="_blank"}。
该PEP建议实现了__enter__() 和__exit__()方法的协议称为上下文管理协议,实现该协议的对象称为上下文管理器。紧跟with关键字之后的表达式是上下文表达式,上下文表达式必须要返回一个上下文管理器。
contextlib
python的标准库提供了contextlib模块来帮助编写__enter__和__exit__方法。contextlib模块提供了contextmanager装饰器,它接受一个生成器(generator),用yield语句把with ... as var的结果输出出去。这样就可以使用with语句了。看下面例子:
from contextlib import contextmanager
@contextmanager
def opened(filename, mode="r"):
file = open(filename, mode)
print('start')
try:
yield file
finally:
file.close()
print('end')
with opened("/etc/passwd") as f:
for line in f:
print(line.strip())
程序在with语句块的执行前后分别打印了start和end。跟踪这个装饰器,看看他的实现源码:
def contextmanager(func):
@wraps(func)
def helper(*args, **kwds):
return _GeneratorContextManager(func, args, kwds)
return helper
他的内部返回一个类的实例_GeneratorContextManager(func, args, kwds),继续跟踪他:
class _GeneratorContextManager(ContextDecorator):
"""Helper for @contextmanager decorator."""
def __init__(self, func, args, kwds):
# 初始化实例,将传入的函数赋值给gen属性,此时函数未执行
self.gen = func(*args, **kwds)
# 依然是赋值
self.func, self.args, self.kwds = func, args, kwds
def _recreate_cm(self):
# 调用装饰器时都必须重新创建实例
return self.__class__(self.func, self.args, self.kwds)
def __enter__(self):
# __enter__()方法
try:
# next()函数是返回一次生成器的值,
# self.gen是传给contextmanager装饰器传给该类的生成器函数哦,
# 看上面的__init__()初始化实例方法
return next(self.gen)
except StopIteration:
# 如果生成器没有更多的元素返回时,会抛出StopIteration的错误。
raise RuntimeError("generator didn't yield") from None
def __exit__(self, type, value, traceback):
# __exit__()方法,这几个参数是什么,看下面
if type is None:
try:
next(self.gen)
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
if value is None:
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration as exc:
return exc is not value
except RuntimeError as exc:
if exc.__cause__ is value:
return False
raise
except:
if sys.exc_info()[1] is not value:
raise
class ContextDecorator(object):
"A base class or mixin that enables context managers to work as decorators."
def __call__(self, func):
@wraps(func)
def inner(*args, **kwds):
with self._recreate_cm():
return func(*args, **kwds)
return inner
_GeneratorContextManager(func, args, kwds)的源码很简单明了,看上面的注释基本上可以理解。该类实现了__enter__() 和__exit__()方法,也就是个上下文管理器。
__enter__() 和__exit__()这两个方法其实很好理解,前者还是在with语句块执行前调用,后者实在语句块执行后调用。
__enter__()
上面的例子中,with语句as关键词后面的f,是什么。如果在with语句块中使用next(f)调用他,他仍然可以正确的输出一行内容。这说明f其实就是file = open()的生成器。他是如何被赋值的。似乎只有在_GeneratorContextManager()类的__enter__()方法中,可以看到他返回了一个next(self.gen),注意这里他并没有被执行。他返回的结果会被赋值给as关键词后面的变量。以一个例子来演示:
class Demo(object):
def hello(self, name):
print('hello, ', name)
def __enter__(self):
print('start')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('end')
with Demo() as h:
h.hello('world')
执行结果:
start
hello, world
end
__exit__()
从上文的几个例子中可以看到,__exit__()方法接收除self外三个参数。从_GeneratorContextManager()类的源码,可以看出他似乎是做的一些上下文管理器的错误处理。仍然以上面的例子,分别打印三个参数来演示:
class Demo(object):
def hello(self, name):
print('hello, ', name)
def __enter__(self):
print('start')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('end')
print('type: ', exc_type)
print('val: ', exc_val)
print('tb: ', exc_tb)
with Demo() as h:
1/0
h.hello('world')
执行结果:
Traceback (most recent call last):
File "C:/Users/Administrator/Desktop/Eager/spider.py", line 71, in <module>
1/0
ZeroDivisionError: division by zero
start
end
type: <class 'ZeroDivisionError'>
val: division by zero
tb: <traceback object at 0x0000000000E34C88>
可以看到,程序报了ZeroDivisionError的错误,再看各个参数的返回的值。他们对比一下不难发现:
- exc_type: 异常类型
- exc_val: 异常信息
- exc_tb: 异常追踪信息
_GeneratorContextManager()类中的__exit__()方法的一些判断语句是不是很容易理解了。
其实__exit__()方法还可以使用return语句,返回False或者True可以对with语句块的错误进行抛出或者隐藏。
参考文章:
廖雪峰的python教程{:target="_blank"}
PEP343{:target="_blank"}