我们在编写代码的时候,通常需要连贯有序地做某件事,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作用域的调用处抛出有可能的异常