一、上下文管理器
在使用Python编程中,可以会经常碰到这种情况:有一个特殊的语句块,在执行这个语句块之前需要先执行一些准备动作;当语句块执行完成后,需要继续执行一些收尾动作。
例如:当需要操作文件或数据库的时候,首先需要获取文件句柄或者数据库连接对象,当执行完相应的操作后,需要执行释放文件句柄或者关闭数据库连接的动作。又如,当多线程程序需要访问临界资源的时候,线程首先需要获取互斥锁,当执行完成并准备退出临界区的时候,需要释放互斥锁。
对于这些情况,Python中提供了上下文管理器(Context Manager)的概念,可以通过上下文管理器来定义/控制代码块执行前的准备动作,以及执行后的收尾动作。
那么在Python中怎么实现一个上下文管理器呢?Python的上下文管理器通常需要和三个概念结合起来:关键字with,两个魔法方法__enter__
,__exit__
。Python中的with语句和上下文管理器,是从2.5版本开始加入到Python语法中的。
也就是说,当我们需要创建一个上下文管理器类型的时候,就需要实现__enter__
和__exit__
方法,这对方法就称为上下文管理协议(Context Manager Protocol),定义了一种运行时上下文环境。下面就是关于这两个方法的具体介绍。
__enter__(self)
定义上下文管理器在with语句创建的块的开始处应该执行的操作。请注意,__enter__
的返回值绑定到with语句的目标或as之后的变量名称。
__exit__(self, exception_type, exception_value, traceback)
定义上下文管理器在块被执行(或终止)之后应该执行的操作。它可以用于处理异常,执行清理,或者在块中的操作之后立即执行某些操作。如果该块成功执行,则exception_type,exception_value和traceback将为None。否则,你可以选择处理异常或让用户处理它;如果你想处理它,确保 __exit__
完成之后返回True。如果你不想让这个异常被上下文管理器处理,就让它发生即可。
二、with语句
在Python中,可以通过with语句来方便的使用上下文管理器,with语句可以在代码块运行前进入一个运行时上下文(执行__enter__
方法),并在代码块结束后退出该上下文(执行__exit__
方法)。
with语句的语法如下:
1 2 |
with EXPR as VAR: BLOCK |
这里就是一个标准的上下文管理器的使用逻辑,稍微解释一下其中的运行逻辑:
1)执行EXPR语句,获取上下文管理器(Context Manager)。
2)调用上下文管理器中的__enter__
方法,该方法执行一些预处理工作。
3)这里的as VAR可以省略,如果不省略,则将__enter__
方法的返回值赋值给VAR。
4)执行代码块BLOCK,这里的VAR可以当做普通变量使用。
5)最后调用上下文管理器中的的__exit__
方法。
6)__exit__
方法有三个参数:exception_type,exception_value,traceback。如果代码块BLOCK发生异常并退出,那么分别对应异常的type、value 和 traceback。否则三个参数全为None。
7)__exit__
方法的返回值可以为True或者False。如果为True,那么表示异常被忽视,相当于进行了try-except操作;如果为False,则该异常会被重新raise。
在Python的内置类型中,很多类型都是支持上下文管理协议的,例如file,thread.LockType,threading.Lock等等。这里我们就以file类型为例,看看with语句的使用。
当需要写一个文件的时候,一般都会通过下面的方式。代码中使用了try-finally语句块,即使出现异常,也能保证关闭文件句柄。
1 2 3 4 5 6 |
logger = open("log.txt", "w") try: logger.write('Hello ') logger.write('World') finally: logger.close() |
Python的内置file类型是支持上下文管理协议的,可以直接通过内建函数 dir() 来查看file支持的方法和属性:
1 2 3 4 5 6 7 8 |
>>> file = open('log.txt', 'r') >>> dir(file) ['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines'] |
所以,可以通过 with 语句来简化上面的代码,代码的效果是一样的,但是使用 with 语句的代码更加的简洁:
1 2 3 |
with open("log.txt", "w") as logger: logger.write('Hello ') logger.write('World') |
对于自定义的类型,可以通过实现__enter__
和__exit__
方法来实现上下文管理器。看下面的代码,自己实现打开文件操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class MyOpen(object): def __init__(self, file_name): """初始化方法""" self.file_name = file_name self.file_handler = None return def __enter__(self): """enter方法,返回file_handler""" print("enter:", self.file_name) self.file_handler = open(self.file_name, "r") return self.file_handler def __exit__(self, exception_type, exception_value, traceback): """exit方法,关闭文件并返回True""" print("exit:", exception_type, exception_value, traceback) if self.file_handler: self.file_handler.close() return True |
__exit__
返回True,是说,这个异常已经被(以某种方法)处理了,别人不用关心这个异常了,其实这里也并没有处理。
使用实例:
1 2 3 |
with MyOpen('log.txt') as f: for line in f: print(line) |
代码很简单,也很容易理解,这里不做过多解释。
看下面的代码,代码中定义了一个MyTimer类型,这个上下文管理器可以实现代码块的计时功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import time class MyTimer(object): def __init__(self, verbose = False): self.verbose = verbose def __enter__(self): self.start = time.time() return self def __exit__(self, exception_type, exception_value, traceback): self.end = time.time() self.secs = self.end - self.start if self.verbose: print('time: {}s'.format(self.secs)) |
下面结合with语句使用这个上下文管理器:
1 2 3 4 5 6 7 8 9 10 11 |
def fib(n): if n in [1, 2]: return 1 else: return fib(n-1) + fib(n-2) >>> with MyTimer(True): ... print(fib(30)) ... 832040 time: 0.35918402671813965s |
在使用上下文管理器中,如果代码块产生了异常,__exit__
方法将被调用,而__exit__
方法又会有不同的异常处理方式。当__exit__
方法退出当前运行的上下文时,会并返回一个布尔值,该布尔值表明了”如果代码块执行中产生了异常,该异常是否须要被忽略”。
1. __exit__
返回False,重新抛出(re-raised)异常到上层
修改前面的例子,在MyTimer类型中加入了一个参数”ignoreException”来表示上下文管理器是否会忽略代码块中产生的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import time class MyTimer(object): def __init__(self, verbose = False, ignoreException = False): self.verbose = verbose self.ignoreException = ignoreException def __enter__(self): self.start = time.time() return self def __exit__(self, exception_type, exception_value, traceback): self.end = time.time() self.secs = self.end - self.start if self.verbose: print('time: {}s'.format(self.secs)) return self.ignoreException |
使用try…except…else处理上下文管理器:
1 2 3 4 5 6 7 |
try: with MyTimer(True, False): raise Exception("Ex4Test") except Exception as e: print("Exception (%s) was caught" % e) else: print("No Exception happened") |
运行这段代码,会得到以下结果,由于__exit__
方法返回False,所以代码块 (with_suite)中的异常会被继续抛到上层代码。
1 2 |
time: 8.106231689453125e-06s Exception (Ex4Test) was caught |
2. __exit__
返回Ture,代码块中的异常被忽略
将代码改为__exit__
返回为True的情况:
1 2 3 4 5 6 7 |
try: with MyTimer(True, True): raise Exception("Ex4Test") except Exception as e: print("Exception (%s) was caught" % e) else: print("No Exception happened") |
运行结果就变成下面的情况,代码块中的异常被忽略了,代码继续运行:
1 2 |
time: 7.152557373046875e-06s No Exception happened |
一定要小心使用__exit__
返回Ture的情况,除非很清楚为什么这么做。
3. 通过__exit__
函数完整的签名获取更多异常信息
对于__exit__
函数,它的完整签名如下,也就是说通过这个函数可以获得更多异常相关的信息。
1 |
__exit__(self, exception_type, exception_value, traceback) |
继续修改上面例子中的__exit__
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class MyTimer(object): def __init__(self, verbose = False, ignoreException = False): self.verbose = verbose self.ignoreException = ignoreException def __enter__(self): self.start = time.time() return self def __exit__(self, exception_type, exception_value, traceback): self.end = time.time() self.secs = self.end - self.start if self.verbose: print('time: {}s'.format(self.secs)) print("exception_type: {}".format(exception_type)) print("exception_value: {}".format(exception_value)) print("traceback: {}".format(traceback)) return self.ignoreException |
这次运行结果中,就显示出了更多异常相关的信息了:
1 2 3 4 5 |
time: 8.296966552734375e-05s exception_type: <class 'Exception'> exception_value: Ex4Test traceback: <traceback object at 0x10e10be88> No Exception happened |
五、内置库contextlib的使用
编写__enter__
和__exit__
仍然很繁琐,因此Python的标准库contextlib提供了更简单的写法,使得上线文管理器更加容易使用。其中包含如下功能:
1)@contextmanager
装饰器contextmanager,该装饰器将一个函数中yield语句之前的代码当做__enter__
方法执行,yield语句之后的代码当做__exit__
方法执行。同时yield返回值赋值给as后的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from contextlib import contextmanager @contextmanager def MyOpen(file): # __enter__方法 print('open file:', file, 'in __enter__') file_handler = open(file, 'r') yield file_handler # __exit__方法 print('close file:', file, 'in __exit__') file_handler.close() return with MyOpen('log.txt') as f: for line in f: print(line) |
@contextmanager这个decorator接受一个generator,用yield语句把with … as var把变量输出出去,然后,with语句就可以正常地工作了。
很多时候,我们希望在某段代码执行前后自动执行特定代码,也可以用@contextmanager实现。例如:
1 2 3 4 5 6 7 8 9 |
@contextmanager def tag(name): print("<%s>" % name) yield print("</%s>" % name) with tag("h1"): print("hello") print("world") |
上述代码执行结果为:
1 2 3 4 |
<h1> hello world </h1> |
代码的执行顺序是:
- with语句首先执行yield之前的语句,因此打印出<h1>;
- yield调用会执行with语句内部的所有语句,因此打印出hello和world;
- 最后执行yield之后的语句,打印出</h1>。
因此,@contextmanager让我们通过编写generator来简化上下文管理。
2)@closing
如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。例如,用with语句使用urlopen():
1 2 3 4 5 6 |
from contextlib import closing from urllib.request import urlopen with closing(urlopen('https://www.python.org')) as page: for line in page: print(line) |
closing也是一个经过@contextmanager装饰的generator,这个generator编写起来其实非常简单:
1 2 3 4 5 6 |
@contextmanager def closing(thing): try: yield thing finally: thing.close() |
它的作用就是把任意对象变为上下文对象,并支持with语句。@contextlib还有一些其他decorator,便于我们编写更简洁的代码。
<参考>