我们在编写代码的时候,通常需要连贯有序地做某件事,1.打开一个文件 2.对文件进行写操作 3.关闭文件。 那么在做步骤2的时候,文件突然关闭的话会导致文件写操作异常,因为文件句柄丢失了。我们当然不希望代码发生这样的错误,所以说,如果有一个工具能使得事务操作具有一定的连贯次序,简单而优雅,就可以避免一些不必要的错误。
在python编程里面常遇到这样的一种场景,在执行某一块代码的时候需要事先做一下准备工作,在代码块结束的时候又需要进行一些扫尾工作,也就是上述的,避免不必要的错误。
一些常见的场景包括在对文件进行操作的时候,需要事先获取文件句柄,在处理完文件后又需要释放文件句柄;还包括数据库的相关操作,比如在对数据库进行读写之前,需要事先做获取数据库连接,在读写操作结束后,还需要释放数据库连接,避免造成僵尸连接等等。
对于此类场景,python提供了context manager的概念,使得代码编写具有很强大的灵活性。
我们先看一个样例:
1 ''' 2 Created on Dec 17, 2017 3 4 @author: kiel 5 ''' 6 7 8 def open_file_func(file_path): 9 10 with open(file_path) as o_file: 11 for o_line in o_file.readlines(): 12 print o_line 13 14 15 if __name__ == '__main__': 16 file_path = '/home/kiel/code_test/test' 17 open_file_func(file_path)
python的上下文管理器通常需要和三个概念结合起来:关键字with,两个魔法方法 __enter__, __exit__.
我们首先来理解一下两个魔法方法:
__enter__(self): 它定义了with声明在语句块开头的地方上下文管理器做的事情,注意__enter__方法的return,通常是与with声明或者with...as <var>绑定的。上述的例子中 __enter__的return与as之后的o_file变量绑定在一起了。
中间是用户正常编写的带执行的语句块block,上例中的block为 11 ~ 12行。
__exit__(self, exc_type, exc_value, exc_traceback): 它定义了with声明后的block执行之后上下文管理器做的事情。它常用来做异常处理,正常工作扫尾,或者block做完之后的一些其他处理(比如记录日志等等操作)。如果block正常执行了,那么__exit__里面的三个异常相关的变量就全部为None(这边解释一下,这三个变量分别代表着,异常类型,异常值,异常栈追踪打印)。如果block里面执行失败了,你可以选择自行处理它,或者直接抛出由上层调用者处理。如果你想处理它,可以在__exit__方法里面处理异常,并确保__exit__能正常返回。如果不想处理,那就什么都不干,直接抛出好了。
这两个魔法方法统称为上下文管理协议(Context Manager Protocol),当我们想使用上下文管理器的时候,可以尝试自己定义这两个方法。
那么现在来看一下with的相关概念。在python里面可以方便的使用with在代码块block执行之前调用__enter__方法进入上下文,在代码块block执行完毕之后调用__exit__方法退出该上下文。它的一个使用的语法规则如下:
1 with context_expr [as var]: 2 with_suite
其中context_expr是上下文管理协议对象,也是上下文管理器对象,负责维护上下文管理环境。
[as var]是一个可选部分,如果有使用这块,__enter__方法的return将与var值绑定,否则,上下文管理器将不再在意__enter__的返回
with_suite,即需要在构建的上下文环境中执行的语句块。
在python里面,有很多内置类型是天然支持上下文管理协议的,比如file,thread.LockType等等。我们可以调用dir来查看这些内置类型的魔法方法,看是否支持上下文管理协议。比如,dir(thread.LockType):
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'acquire', 'acquire_lock', 'locked', 'locked_lock', 'release', 'release_lock']
至此,上下文管理器的轮廓就已经介绍结束了。下面我们可以自定义一个上下午管理器,用来处理一些事务。
我们可以做一个简单的计时上下文管理器,用来对代码块处理时间进行打印:
1 ''' 2 Created on Dec 17, 2017 3 4 @author: kiel 5 ''' 6 7 import datetime 8 import time 9 10 def open_file_func(file_path): 11 12 with open(file_path) as o_file: 13 for o_line in o_file.readlines(): 14 print o_line 15 16 17 def get_target_value(file_path): 18 rt_list = [] 19 with open(file_path) as o_file: 20 for o_line in o_file.readlines(): 21 print o_line 22 rt_list.append(int(o_line)) 23 return rt_list 24 25 26 def fb_func(target): 27 if target == 0 or target == 1: 28 return 1 29 elif target < 0: 30 raise ValueError('target (%s) is invalid' % target) 31 else: 32 little_one = 1 33 giant_one = 1 34 for _ in range(target-1): 35 rv = little_one + giant_one 36 little_one = giant_one 37 giant_one = rv 38 return rv 39 40 41 class TimeThis(object): 42 43 def __init__(self, exclusive=False): 44 self.exclusive = exclusive 45 if self.exclusive: 46 print ("try to acquire exclusive lock at (%s)" 47 % str(datetime.datetime.now())) 48 49 def __enter__(self): 50 self.begin_time = time.time() 51 return self 52 53 def __exit__(self, exc_tp, exc_vl, exc_tb): 54 self.end_time = time.time() 55 if self.exclusive: 56 print ("try to release exclusive lock at (%s)" 57 % str(datetime.datetime.now())) 58 print ("func exec time_elapsed (%s) seconds" 59 % str(self.end_time - self.begin_time)) 60 61 62 if __name__ == '__main__': 63 file_path = '/home/kiel/code_test/test' 64 with TimeThis(True) as time_lock: 65 print str(time_lock.exclusive) + " in kiel block" 66 input_list = get_target_value(file_path) 67 for i in input_list: 68 print 'target (%s) result (%s)' % (i, fb_func(i)) 69 print 'out kiel block' 70 71 72 73 74 75 76
这个代码的逻辑是,从一个文件里面打开读取一些数值,根据这些数值求出斐波那契数列里面对应位置的值。
读取文件时,将某一行的输出打印并转成整型传入列表返回。
求取斐波那契数列时,以递推的方式求取,效率更高。
在进入上下文管理器的同时,打印锁的获取并统计block的开始执行时间,在离开上下文管理器的时候,打印锁的释放并统计整个block的耗时。
它的执行结果如下:
1 try to acquire exclusive lock at (2017-12-17 16:06:16.368425) 2 True in kiel block 3 5 4 5 10 6 7 50 8 9 100 10 11 target (5) result (8) 12 target (10) result (89) 13 target (50) result (20365011074) 14 target (100) result (573147844013817084101) 15 try to release exclusive lock at (2017-12-17 16:06:16.368824) 16 func exec time_elapsed (0.000264883041382) seconds 17 out kiel block
下面是魔法方法__exit__对异常的处理部分,还记得前面怎么说的嘛,__exit__在遇到异常的时候返回True就忽略所有可能的异常,否则呢,则向上抛出异常打印,那么我把TimeThis类改造一下:
1 ''' 2 Created on Dec 17, 2017 3 4 @author: kiel 5 ''' 6 7 import datetime 8 import time 9 10 def open_file_func(file_path): 11 12 with open(file_path) as o_file: 13 for o_line in o_file.readlines(): 14 print o_line 15 16 17 def get_target_value(file_path): 18 rt_list = [] 19 with open(file_path) as o_file: 20 for o_line in o_file.readlines(): 21 print o_line 22 rt_list.append(int(o_line)) 23 return rt_list 24 25 26 def fb_func(target): 27 if target == 0 or target == 1: 28 return 1 29 elif target < 0: 30 raise ValueError('target (%s) is invalid' % target) 31 else: 32 little_one = 1 33 giant_one = 1 34 for _ in range(target-1): 35 rv = little_one + giant_one 36 little_one = giant_one 37 giant_one = rv 38 return rv 39 40 41 class TimeThis(object): 42 43 def __init__(self, exclusive=False, ignoreexc=False): 44 self.exclusive = exclusive 45 self.ignoreexc = ignoreexc 46 if self.exclusive: 47 print ("try to acquire exclusive lock at (%s)" 48 % str(datetime.datetime.now())) 49 50 def __enter__(self): 51 self.begin_time = time.time() 52 return self 53 54 def __exit__(self, exc_tp, exc_vl, exc_tb): 55 self.end_time = time.time() 56 if self.exclusive: 57 print ("try to release exclusive lock at (%s)" 58 % str(datetime.datetime.now())) 59 print ("func exec time_elapsed (%s) seconds" 60 % str(self.end_time - self.begin_time)) 61 if exc_tb and exc_tp and exc_vl: 62 print "Exception Type is (%s)" % str(exc_tp) 63 print "Exception Value is (%s)" % str(exc_vl) 64 print "Exception Traceback is (%s)" % str(exc_tb) 65 if self.ignoreexc: 66 return True 67 68 def func_main(ignore_exc=True): 69 file_path = '/home/kiel/code_test/test' 70 with TimeThis(True, ignore_exc) as time_lock: 71 print str(time_lock.exclusive) + " in kiel block" 72 input_list = get_target_value(file_path) 73 for i in input_list: 74 print 'target (%s) result (%s)' % (i, fb_func(i)) 75 print 'out kiel block' 76 77 78 if __name__ == '__main__': 79 try: 80 func_main(False) 81 except Exception as e: 82 print 'Main1: func_main err(%s)' % str(e) 83 finally: 84 pass 85 try: 86 func_main(True) 87 except Exception as e: 88 print 'Main2: func_main err(%s)' % str(e) 89 else: 90 print "Main2: func_main nothing happend" 91 92 93 94 95 96 97 98
那么打印就是:
1 pydev debugger: starting (pid: 19112) 2 try to acquire exclusive lock at (2017-12-17 16:27:32.487803) 3 True in kiel block 4 5 5 6 10 7 8 50 9 10 100 11 12 -1 13 14 target (5) result (8) 15 target (10) result (89) 16 target (50) result (20365011074) 17 target (100) result (573147844013817084101) 18 try to release exclusive lock at (2017-12-17 16:27:32.488160) 19 func exec time_elapsed (0.000257015228271) seconds 20 Exception Type is (<type 'exceptions.ValueError'>) 21 Exception Value is (target (-1) is invalid) 22 Exception Traceback is (<traceback object at 0x7ff6c2b03b00>) 23 Main1: func_main err(target (-1) is invalid)
24 try to acquire exclusive lock at (2017-12-17 16:27:32.488266) 25 True in kiel block 26 5 27 28 10 29 30 50 31 32 100 33 34 -1 35 36 target (5) result (8) 37 target (10) result (89) 38 target (50) result (20365011074) 39 target (100) result (573147844013817084101) 40 try to release exclusive lock at (2017-12-17 16:27:32.488738) 41 func exec time_elapsed (0.000420093536377) seconds 42 Exception Type is (<type 'exceptions.ValueError'>) 43 Exception Value is (target (-1) is invalid) 44 Exception Traceback is (<traceback object at 0x7ff6c2b03c68>) 45 out kiel block 46 Main2: func_main nothing happend
最后总结一下咯:
- 使用with 执行 context_expr获取上下文管理器环境(说成新的作用域也不为过),如果有as var,则将__enter__的方法return的结果赋值给var
- 执行context manager作用域下的用户自定义代码
- 如果__exit__的return结果为True,则忽略所有用户自定义代码中的异常,否则,则向创建with作用域的调用处抛出有可能的异常