上一篇文章中,解决*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"}