(转)Python中的上下文管理器和Tornado对其的巧妙应用
原文:https://www.binss.me/blog/the-context-manager-of-python-and-the-applications-in-tornado/
上下文是什么?
在协程中,我将上下文理解为“操作执行时需要的一个特定的执行环境“。在该环境中,“上文”提供该操作需要的变量等信息,“下文“对操作执行返回的结果进行进一步的处理。
比如:
| def add(a, b): | |
| op = '+' | |
| result = yield cal(op, a, b) | |
| print('a + b = %d', result) |
该协程用于计算a+b的结果。对于具体的操作cal,“上文”提供op、a、b三个变量作为函数调用的参数,“下文“将cal返回的值打印出来。
“上下文管理器”中的“上下文”是否和协程上下文的定义一致呢?
John Jacobsen将定义为如下:
Context managers are a way of allocating and releasing some sort of resource exactly where you need it.
官方的定义如下:
A context manager is an object that defines the runtime context to be established when executing a with statement. The context manager handles the entry into, and the exit from, the desired runtime context for the execution of the block of code.
上下文管理器的典型应用情景有文件访问:需要打开文件(allocating),然后对文件进行操作,最后关闭文件(releasing)。还有线程锁:需要加锁(allocating),然后执行线程安全操作,最后释放锁(releasing)。可见“上下文管理器”中“上下文”和协程中“上下文”的定义一致,都是“操作执行时需要的一个特定的执行环境”。对于文件访问,提供了可以直接读写文件的环境(打开的文件描述符);对于锁应用,提供了无竞争的安全操作环境(有锁,同时只能有一个线程执行临界区代码)。所以上下文管理器,就是该特定的执行环境的提供者。
在Python中,要定义上下文管理器,只需实现上下文管理器协议:实现contextmanager.__enter__()和contextmanager.__exit__(exc_type, exc_val, exc_tb)函数,如:
| class Context: | |
| def __init__(self, {arguments}): | |
| pass | |
| def __enter__(self): | |
| {setup} | |
| return {value} | |
| def __exit__(self): | |
| {cleanup} |
在PEP 343中提供了方便使用上下文管理器的语法糖——with statement:
| with Context() as v: | |
| {body} |
这段代码在执行时,先是调用Context的构造函数以生成上下文管理对象,然后调用__enter__函数提供“上文”,并将`__enter__`的返回值赋给as后跟着的变量v以供{body}使用。有了“上文”后,执行{body}。执行完毕后,自动调用__exit__()执行“下文”操作,然后结束。如果在执行{body}的过程中有异常抛出怎么办?看到__exit__()的三个参数了吗?它们保存了{body}抛出的异常类型、异常值和异常相关信息,于是我们可以在__exit__()内对异常进行处理,并通过return True阻止异常向上传播。如果没有异常抛出,那么它们都为None。
除了上述的方法,我们还可以通过@contextlib.contextmanager装饰器来定义上下文管理器。@contextlib.contextmanager可以将生成器(Generator)转换成上下文管理器:
| @contextlib.contextmanager | |
| def some_generator({arguments}): | |
| {setup} | |
| try: | |
| yield {value} | |
| finally: | |
| {cleanup} | |
| with some_generator({arguments}) as {variable}: | |
| {body} |
等价于上述的:
| with Context({arguments}) as {variable}: | |
| {body} |
本质上等价于:
| {setup} | |
| try: | |
| {variable} = {value} | |
| {body} | |
| finally: | |
| {cleanup} |
需要注意的是,yield语句需要使用try...finally语句包起来。因为如果yield语句抛出异常,yield之后的语句将不会执行,这与上下文管理器会调用__exit__()执行“下文”操作不一致。
至此我们可以发现上下文管理器的优点:封装上下文环境,便于复用;使用简单,with后即进入上下文环境。
Tornado中的stack_context
Tornado的stack_context是对Python上下文管理器的巧妙应用。
在Tornado中,如果通过add_callback等在ioloop的添加了异步回调函数,当回调函数抛出异常时,无法通过在调用add_callback处捕捉到该异常,如:
| def callback(): | |
| raise Exception('in callback') | |
| def func(): | |
| ioloop.add_callback(callback) | |
| ioloop = tornado.ioloop.IOLoop.instance() | |
| try: | |
| func() | |
| except: | |
| print('catch the Exception') | |
| ioloop.start() |
虽然是func导致了callback的调用,进而导致异常的抛出。但实际上callback在func中被add_callback后并没有立即执行,只是被放到IOLoop中,等到适当的时机执行。当callback执行时,它的环境早已不是“调用时“(func)的环境,因此对func的try...except无法捕捉到异常,自然“catch the Exception”也就不会被打印。这样的结果显然与我们过去写同步代码的经验不符,我们希望找到一个方法,使得“调用时”和“执行时“能够保持相同的环境。
为了解决这个问题,最直观的办法是对func和callback都使用wrap进行包装,这样就保证callback在“调用时”和“执行时“的环境相同,即都处于
| try: | |
| ... | |
| except: | |
| print('catch the Exception'): |
的包裹之下:
| def wrap(callback): | |
| def wrapper(*args, **kwargs): | |
| try: | |
| callback() | |
| except: | |
| print('catch the Exception') | |
| return wrapper | |
| def callback(): | |
| raise Exception('in callback') | |
| def func(): | |
| ioloop.add_callback(wrap(callback)) | |
| ioloop = tornado.ioloop.IOLoop.instance() | |
| wrap(func)() | |
| ioloop.start() |
查看ioloop.py的add_callback函数,可以发现Tornado就是使用了这种方案:
| ...... | |
| self._callbacks.append(functools.partial(stack_context.wrap(callback), *args, **kwargs)) |
看看Tornado怎么做
Tornado通过一个名为StackContext的对象实现对“调用时”上下文环境的保存。
StackContext的定义如下:
| class StackContext(object): | |
| def __init__(self, context_factory): | |
| self.context_factory = context_factory | |
| self.contexts = [] | |
| self.active = True | |
| def _deactivate(self): | |
| self.active = False | |
| def enter(self): | |
| context = self.context_factory() | |
| self.contexts.append(context) | |
| # 真正进入上下文 | |
| context.`__enter__()` | |
| def exit(self, type, value, traceback): | |
| context = self.contexts.pop() | |
| # 真正退出上下文 | |
| context.__exit__(type, value, traceback) | |
| def __enter__(self): | |
| self.old_contexts = _state.contexts | |
| # 当前状态入栈 | |
| self.new_contexts = (self.old_contexts[0] + (self,), self) | |
| _state.contexts = self.new_contexts | |
| try: | |
| self.enter() | |
| except: | |
| _state.contexts = self.old_contexts | |
| raise | |
| return self._deactivate | |
| def __exit__(self, type, value, traceback): | |
| try: | |
| self.exit(type, value, traceback) | |
| finally: | |
| final_contexts = _state.contexts | |
| # 当前状态出栈 | |
| _state.contexts = self.old_contexts | |
| if final_contexts is not self.new_contexts: | |
| raise StackContextInconsistentError( | |
| 'stack_context inconsistency (may be caused by yield ' | |
| 'within a "with StackContext" block)') | |
| self.new_contexts = None |
StackContext实现了对上下文管理器的包装,构造函数接受一个参数context_factory,这是一个上下文管理器的工厂方法。调用context_factory可以获得上下文管理器,然后即可通过__enter__()和__exit__()进入和退出保存的上下文环境。StackContext也实现了上下文管理器协议,目的是为了和上下文管理器的行为保持一致。
为什么要多此一举,对原有的上下文环境进行包装呢?如果只需维护一个上下文环境,确实不必如此,wrap函数直接通过__enter__()即可进入保存的“调用时“上下文环境。但是当“调用时”有多个嵌套的上下文环境时(比如A嵌套B,B又嵌套了C),问题来了:如何保证wrap函数能够按顺序调用相应的__enter__()来进入“调用时“上下文环境呢?StackContext解决的正是这个问题。
先看线程局部变量_state,它的成员contexts是一个二元tuple:
| class _State(threading.local): | |
| def __init__(self): | |
| self.contexts = (tuple(), None) | |
| _state = _State() |
在StackContext的__enter__()中,StackContext将自身加入到该contexts[0]的尾端,并将contexts[1]设置为自身。如果把_state理解为上下文嵌套中的一个状态,那么contexts[0]为当前状态下的StackContext栈,保存了从最外层上下文到当前上下文的所有StackContext;而contexts[1]由于保存的是最后一个StackContext,可以认为是栈顶指针。因此如果想进入到“调用时“的上下文环境,只需将“调用时“的_state取出,然后对contexts[0]从头到尾调用enter()即可。更为巧妙的是,为了能够建立状态之间的联系,每一个StackContext在保存当前状态self.new_contexts的同时,也保存了上一个状态self.old_contexts。
画图可以方便理解。以A嵌套B,B又嵌套了C这种环境为例:
![]()
在调用do_something时,状态为State C,即((A, B, C), C)。wrap()将State C保存下来,然后在“执行时“调用A、B、C的enter()即可使do_something获得和“调用时”一样的State C上下文。我们来看看wrap具体是怎么做的:
| def wrap(fn): | |
| # 如果已经包装过,直接返回 | |
| if fn is None or hasattr(fn, '_wrapped'): | |
| return fn | |
| # 保存包装时的上下文状态 | |
| # 在闭包中,无法修改外部作用域的局部变量,将外部作为左值将会被认为是闭包内部的局部变量 | |
| # 因此为了在闭包(wrapped)内部对_state.contexts进行修改,将其放入list中,这样即使list不能被修改,但list内的元素可以被修改 | |
| cap_contexts = [_state.contexts] | |
| # 如果包装时的上下文管理器栈为空(没有上下文),则执行时无需进入上下文,这里进行空包装即可 | |
| if not cap_contexts[0][0] and not cap_contexts[0][1]: | |
| # 调用时,取出包装时的上下文管理器容器,然后调用函数,调用完后恢复执行时的上下文管理器容器 | |
| def null_wrapper(*args, **kwargs): | |
| try: | |
| current_state = _state.contexts | |
| _state.contexts = cap_contexts[0] | |
| return fn(*args, **kwargs) | |
| finally: | |
| _state.contexts = current_state | |
| # 设置包装标志,防止重复包装 | |
| null_wrapper._wrapped = True | |
| return null_wrapper | |
| # 如果上下文管理器栈不为空,需要进入“调用时”的上下文 | |
| def wrapped(*args, **kwargs): | |
| ret = None | |
| # 取出包装时的所有上下文管理器,移除那些已经设置为失效的StackContext,然后从头到尾对StackContext调用enter以恢复包装时的上下文 | |
| try: | |
| # 保存执行时的上下文状态,用于最后恢复 | |
| current_state = _state.contexts | |
| # 移除调用过_deactivate的StackContext | |
| cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0]) | |
| # 设置为调用时的上下文状态 | |
| _state.contexts = contexts | |
| exc = (None, None, None) | |
| top = None | |
| last_ctx = 0 | |
| stack = contexts[0] | |
| # 对调用时的上下文管理器栈,从头到尾调用enter来设置上下文 | |
| for n in stack: | |
| try: | |
| n.enter() | |
| last_ctx += 1 | |
| except: | |
| # 如果在设置上下文期间抛出异常,设置top为上一个管理器,因为当前管理器没有成功进入 | |
| exc = sys.exc_info() | |
| top = n.old_contexts[1] | |
| # 如果设置上下文都没抛出异常,调用该函数(此时执行时上下文已和调用时的一致) | |
| if top is None: | |
| try: | |
| ret = fn(*args, **kwargs) | |
| except: | |
| exc = sys.exc_info() | |
| # 如果在执行函数期间抛出异常,设置top为当前上下文管理器(栈顶),因为当前管理器已成功进入 | |
| top = contexts[1] | |
| # 处理异常,top为应该处理异常的上下文管理器 | |
| if top is not None: | |
| exc = _handle_exception(top, exc) | |
| else: | |
| # 如果没异常,反向调用exit以退出之前进入的上下文 | |
| while last_ctx > 0: | |
| last_ctx -= 1 | |
| c = stack[last_ctx] | |
| try: | |
| c.exit(*exc) | |
| except: | |
| # 如果在退出下文期间抛出异常,设置top为上一个管理器,因为当前管理器已经退出过了 | |
| exc = sys.exc_info() | |
| top = c.old_contexts[1] | |
| break | |
| else: | |
| top = None | |
| # 处理异常 | |
| if top is not None: | |
| exc = _handle_exception(top, exc) | |
| # 如果异常还是没被处理,只能抛出 | |
| if exc != (None, None, None): | |
| raise_exc_info(exc) | |
| finally: | |
| # 恢复执行时的上下文状态 | |
| _state.contexts = current_state | |
| return ret | |
| # 设置包装标志,防止重复包装 | |
| wrapped._wrapped = True | |
| return wrapped |
可以看到,wrap在调用时生成函数闭包,把“调用时”的上下文状态_state.contexts保存起来,然后在“执行时”将上下文状态取出,从头到尾对StackContext栈中的StackContext调用enter()从而使其管理的上下文管理器的__enter__()得到调用,设置相应的上下文。如果没有异常抛出,此时“执行时”上下文已和“调用时”的一致,可以放心地调用函数。如果调用成功,从尾到头对StackContext栈中的StackContext调用exit()从而使其管理的上下文管理器的__exit__()得到调用,退出相应的上下文。注意如果在enter或exit的过程中有异常抛出,那么当前状态的上一个状态栈顶一个StackContext将作为异常的第一处理者对异常进行处理。而如果在函数调用的过程中有异常抛出,那么当前状态的栈顶StackContext将作为异常的第一处理者对异常进行处理。处理异常的过程可以看成是一个冒泡的过程:
| def _handle_exception(tail, exc): | |
| while tail is not None: | |
| try: | |
| if tail.exit(*exc): | |
| exc = (None, None, None) | |
| except: | |
| exc = sys.exc_info() | |
| tail = tail.old_contexts[1] | |
| return exc |
从第一处理者(处理异常的最内层)开始,调用exit,希望能够处理掉该异常。如果异常没有被处理,那么通过old_contexts[1]获得上一层的StackContext让其处理,直到到达最外层的StackContext为止。该冒泡过程符合我们以往对于异常传播的经验:
![]()
另外一个设计巧妙的地方在于用户可以通过调用_deactivate()将StackContext设置为失效,这样在进入上下文的过程中将忽略此层的上下文。如何让用户调用_deactivate()呢?这里再次利用到了with语法糖的特性,只需在上下文管理器的__enter__()中返回该函数,即可通过as的形式赋给外部变量,并在with statement中使用:
| with StackContext() as deactivate: | |
| deactivate() |
最后,我们尝试使用Tornado的stack_context来处理异步回调中抛出异常的问题:
| def callback(): | |
| print('Run callback') | |
| raise Exception('in callback') | |
| @contextlib.contextmanager | |
| def A(): | |
| print("Enter A context") | |
| try: | |
| yield | |
| except Exception as e: | |
| print("A catch the exception: %s" % e) | |
| print("Exit A context") | |
| @contextlib.contextmanager | |
| def B(): | |
| print("Enter B context") | |
| try: | |
| yield | |
| except Exception as e: | |
| print("B catch the exception: %s" % e) | |
| print("Exit B context") | |
| @contextlib.contextmanager | |
| def C(): | |
| print("Enter C context") | |
| try: | |
| yield | |
| except Exception as e: | |
| print("C catch the exception: %s" % e) | |
| print("Exit C context") | |
| ioloop = tornado.ioloop.IOLoop.instance() | |
| with StackContext(A): | |
| with StackContext(B): | |
| with StackContext(C): | |
| ioloop.add_callback(callback) | |
| ioloop.start() |
输出如下:
| Enter A context | |
| Enter B context | |
| Enter C context | |
| Exit C context | |
| Exit B context | |
| Exit A context | |
| Enter A context | |
| Enter B context | |
| Enter C context | |
| Run callback | |
| C catch the exception: in callback | |
| Exit C context | |
| B catch the exception: in callback | |
| Exit B context | |
| A catch the exception: in callback | |
| Exit A context | |
| ERROR:tornado.application:Exception in callback functools.partial(.wrapped at 0x7fbb52740048>) | |
| Traceback (most recent call last): | |
| File "/usr/local/lib/python3.5/dist-packages/tornado/ioloop.py", line 600, in _run_callback | |
| ret = callback() | |
| File "/usr/local/lib/python3.5/dist-packages/tornado/stack_context.py", line 343, in wrapped | |
| raise_exc_info(exc) | |
| File "", line 3, in raise_exc_info | |
| File "/usr/local/lib/python3.5/dist-packages/tornado/stack_context.py", line 314, in wrapped | |
| ret = fn(*args, **kwargs) | |
| File "ttt.py", line 21, in callback | |
| raise Exception('in callback') | |
| Exception: in callback |
前六行输出于“调用时”,在with的嵌套下我们调用了上下文管理器A、B、C的__enter__(),在ioloop中添加回调函数callback后调用A、B、C的__exit__()退出。到了“执行时”,6-9行为callback设置上下文环境,然后调用callback函数。callback抛出了异常,第一处理者为C,于是在_handle_exception的冒泡机制下异常从C到B到A,最后到达全局层面。这时问题又来了。不是说可以通过设置上下文管理器__exit__()的返回值为True来阻止异常继续抛出的吗?阅读源码,发现以下内容:
| if tail.exit(*exc): | |
| exc = (None, None, None) | |
| ... | |
| def exit(self, type, value, traceback): | |
| context = self.contexts.pop() | |
| context.__exit__(type, value, traceback) |
对于StackContext来说,调用exit()是没有返回值的,于是exc内的元素永远不会设置为None,设置__exit__()的返回值并没有什么卵用。如果改为这样就可以满足我们的期望:
| def exit(self, type, value, traceback): | |
| context = self.contexts.pop() | |
| return context.__exit__(type, value, traceback) |
正准备Push之际,查看了stack_context_test的相关测试用例,并没有使用StackContext来处理异常的用例。对待异常,其使用了ExceptionStackContext来处理,在ExceptionStackContext的exit()中,将exception_handler的返回值设置为True可以阻止异常的继续抛出:
| def exit(self, type, value, traceback): | |
| if type is not None: | |
| return self.exception_handler(type, value, traceback) |
所以估计StackContext并非是用来传递异常的,而是用来传递相关环境的。想至此,默默地取消了Push。
总结
Python中的上下文管理器和with语法糖极大地方便了我们的编程,在封装了上下文管理器后,我们只需专注于写with statement即可。Tornado的stack_context对上下文管理器的应用更是巧妙到了极致,其通过定义栈式上下文管理器,实现了上下文嵌套环境下的上下文设置与恢复。
参考
https://en.wikibooks.org/wiki/Python_Programming/Context_Managers
http://www.zouyesheng.com/context-in-async-env.html
(转)Python中的上下文管理器和Tornado对其的巧妙应用的更多相关文章
- Python中的上下文管理器和with语句
Python2.5之后引入了上下文管理器(context manager),算是Python的黑魔法之一,它用于规定某个对象的使用范围.本文是针对于该功能的思考总结. 为什么需要上下文管理器? 首先, ...
- 深入理解 Python 中的上下文管理器
提示:前面的内容较为基础,重点知识在后半段. with 这个关键字,对于每一学习Python的人,都不会陌生. 操作文本对象的时候,几乎所有的人都会让我们要用 with open ,这就是一个上下文管 ...
- python中实现上下文管理器的两种方法
上下文管理器: python中实现了__enter__和__exit__方法的对象就可以称之为上下文管理器 实现方法一举例: def File(object): def __init__(self, ...
- Python中的上下文管理器(contextlib模块)
上下文管理器的任务是:代码块执行前准备,代码块执行后收拾 1 如何使用上下文管理器: 打开一个文件,并写入"hello world" filename="my.txt&q ...
- python中利用上下文管理器来实现mysql数据库的封装
from pymysql import connect class DB(object): def __init__(self, password, database): # 1.连接数据库 self ...
- python中的上下文管理器
刚刚看了vamei大神的上下文管理器博客,理解如下: 其实我自己经常用到上下文管理器,尤其是在打开文件的时候,如果自己比较懒,不想手工打上f.close(),使用上下文管理器就ok拉. 上下文管理器就 ...
- Python深入02 上下文管理器
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 上下文管理器(context manager)是Python2.5开始支持的一种语 ...
- 【Python】【上下文管理器】
"""#[备注]#1⃣️try :仅当try块中没有异常抛出时才运行else块.#2⃣️for:仅当for循环运行完毕(即for循环没有被break语句终止)才运行els ...
- Python异常处理与上下文管理器
Python异常处理 异常与错误 错误 可以通过IDE或者解释器给出提示的错误opentxt('a.jpg','r') 语法层面没有问题,但是自己代码的逻辑有问题if age>18: print ...
随机推荐
- sizeof新用法(c++11)
1.概念 1)sizeof是关键字,也是运算符,用来求对象占用空间的大小,返回字节数 2)c++11允许使用作用域运算符(::)来获取类中成员的大小,以前只允许先创建一个类的对象,通过类对象访问成员得 ...
- SVN被锁定的几种解决方法
用SVN经常出现被锁定而无法提交的问题,选择解锁又提示没有文件被锁定,很是头疼.这里整理了一下SVN被锁定的几种解决方法: 1.出现这个问题后使用“清理”即"Clean up"功能 ...
- Mysql分析优化查询的方式
一:查询语句分析 1.通过create index idx_colunmsName on tableName(columns)为某个表的某些字段创建索引,注意主键和唯一键都会自动创建索引: 如为表st ...
- python 基础_ 打印输出 循环分支2
一.在python3中的打印输出 1.输出字符串是print("hello world!!!") #输出字符串的时候可以是单引号括起来,也可以是双引号括起来.区别在于 2.输出变量 ...
- 诡异的 ERROR 1045 (28000): Access denied for user 错误
问题描述: 用户已建,权限已赋予.long long ago这个用户是可以正常访问的,但是今天它就不能访问了.报错如下: ERROR 1045 (28000): Access denied for u ...
- 【慕课网实战】Spark Streaming实时流处理项目实战笔记十一之铭文升级版
铭文一级: 第8章 Spark Streaming进阶与案例实战 黑名单过滤 访问日志 ==> DStream20180808,zs20180808,ls20180808,ww ==> ( ...
- Matlab用mpeaks函数求峰值点坐标
clear;clc;close all % 初始化 m = [-6,-2,0,2,4,6]; sigma = [1,1,0.5,0.25,0.6,2]; h = [1,2,3,2,2.13,3.14] ...
- kepware http接口 OCaml
读取某变量的值 open Cohttp_lwt_unix open Cohttp open Lwt let uri = Uri.of_string "http://127.0.0.1:393 ...
- Scala_数据结构
数据结构 容器(Collection) Scala提供了一套丰富的容器(collection)库,包括列表 (List).数组(Array).集合(Set).映射(Map)等 根据容器中元素的组织方式 ...
- [kuangbin]树链剖分 D - 染色
https://vjudge.net/contest/251031#problem/Dhttps://blog.csdn.net/kirito_acmer/article/details/512019 ...