Python上下文管理器详细使用教程
lijiachang8 人气:0with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下问题。
这样做能避免错误并减少样板代码,因此API能更安全,更易使用。除了自动关闭文件之外,with块还有很多用途。
上下文管理器和with块
上下文管理器对象目的是管理with语句,就像迭代器的存在是为了管理for语句一样。
with语句的目的是简化try/finnally模式。
这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码是由于异常、return语句、sys.exit()调用而终止的,也都会执行指定的finally操作。finally子句中通常存放用于释放重要资源,或者还原临时变更的状态。
上下文管理器协议包含__enter__和__exit__两个方法。
with语句开始运行时,会在上下文管理对象上调用__enter__方法;with语句运行结束之后,会在上下文管理对象上调用__exit__方法,以扮演finally子句的角色。
示例,把文件对象当做上下文管理器对象使用。
with open('cafe.txt') as fp: src = fp.read(60) print(fp) # fp变量依旧可以用 print(fp.closed, fp.encoding) # 读取fp对象的属性 print(fp.read()) # 但是执行fp的IO操作会异常
打印
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
True cp936
Traceback (most recent call last):
File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 8, in <module>
print(fp.read())
ValueError: I/O operation on closed file.
知识点:
- fp变量在上下文管理器之外,依旧存在,可以读取fp对象属性。因为with块于函数和模块不同,没有定义新的作用域。
- 但是 不能在fp上再执行IO操作,因为在with块的末尾,已经调用了TextIOWrapper__exit__方法把文件关闭了。
执行with后面的表达式的结果是上下文管理器对象,不过,把值绑定到目标变量(as后的变量)是在上下文管理器对象上调用__enter__方法的结果。
不管控制流程以哪种方式退出with块,都会在上下文管理器对象上调用__exit__方法,而不是在__enter__方法返回的对象上调用。
with语句的as子句是可选的。对于像open这样的函数来说,必须加上as子句,以便获取文件的对象引用。不过一些上下文管理器对象会返回None,因为没有什么有用的对象给用户提供。
示例,实现一个LookingGlass类,上下文管理器
class LookingGlass: """镜子:看到的字是反的""" def __enter__(self): import sys self.original_write = sys.stdout.write # 把原始的[屏幕打印输出]函数保存到一个实例属性中,供以后使用 sys.stdout.write = self.reverse_write # 猴子补丁:替换成自己的方法实现 return "ABCD" def reverse_write(self, text): self.original_write(text[::-1]) # 调用原始的屏幕打印,但是把内容反转 def __exit__(self, exc_type, exc_val, exc_tb): import sys sys.stdout.write = self.original_write # 还原成原始的函数功能 if exc_type is ZeroDivisionError: print('Do not divide by Zero!') return True # 告诉解释器,异常已经处理 # 其他的情况返回None,交给Python抛出异常 with LookingGlass() as what: print('lijiachang') print(what) print(what) print('back to normal')
打印
gnahcaijil
DCBA
ABCD
back to normal
知识点:
- sys.stdout.write 是标准屏幕打印输出。要注意如果暂时缓存其他对象改变功能,记得最后还原成原来的版本。
- Python调用__enter__方法时,除了self之外不会传入其他参数
- 如果一切正常,Python调用__exit__方法时,传入的是None,None,None;如果抛出了异常,这三个参数是异常数据。如下:
exec_type: 异常类名称。如ZeroDivisionError
exc_value: 异常实例。有时会有参数传递给异常构造方法,例如错误信息,这些参数使用exc_value.args获取
traceback: traceback对象。
补充,在try/finally语句的finally块中调用sys.exc_info()得到的就是__exit__接收的这三个参数。
- 在__exit__中返回True是告诉解释器,异常已经处理了。如果返回True之外的值,比如默认的None,with块中的异常会向上冒泡,让Python来抛出。
In [44]: manager = LookingGlass()
In [45]: manager
Out[45]: <__main__.LookingGlass at 0xac6a970>
In [46]: m = manager.__enter__()
In [47]: m == "ABCD"
Out[47]: eurT
In [49]: m
Out[49]: 'DCBA'
In [50]: manager
Out[50]: >079a6cax0 ta ssalGgnikooL.__niam__<
In [54]: manager.__exit__(None, None, None)
In [55]: m
Out[55]: 'ABCD'
可以看到在调用__enter__之后,所有的标准打印输出,都会反转。因为stdout的所有输出都经过了__enter__方法中打补丁的reverse_write方法实现。
contextlib模块
Python标准库文档中的contextlib模块,提供了自定义上下文管理器的一些函数
- closing :如果对象提供了close()方法,但没有实现__enter__/__exit__方法,可以使用这个函数来构建上下文管理器。
- suppress : 构建临时忽略指定异常的上下文。
- @contextmanager :这个装饰器可以把简单的生成器变为上下文管理器,这样就不需要创建类来实现管理器协议了。
- ContextDecorate :这是个基类,用于编写类可以继承他,用于定义基于类的上下文管理器。也可以用于装饰器函数,在受管理的上下文中运行整个函数。
- ExitStack : 这个上下文管理器能进入多个上下文管理器。with块结束时,按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法。如果事先不知道 with块要进入多少个上下文管理器,可以使用这个类。
使用最广泛的还是@contextmanager装饰器。要注意,这个装饰器与迭代无关,却要使用yeild关键字。
@contextmanager 装饰器
使用@contextmanager装饰器呢个减少创建上下文管理器的代码量,因为不用编写一个完整的类,不用定义__enter__和__exit__方法,只需要一个实现yeild语句的生成器,生成想让__enter__方法返回的值。
其中yeild语句的作用是把函数的定义体分为两部分:
yeild语句前面的代码在with块开始时(即解释器调用__enter__方法时)执行。
yeild语句后面的代码在with块结束时(即调用__exit__方法时)执行。
示例,使用生成器实现上下文管理器
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write # 把原始的[屏幕打印输出]函数保存到一个实例属性中,供以后使用 def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write # 猴子补丁:替换成自己的方法实现 yield "ABCD" # 这个值会绑定到as后的变量上 sys.stdout.write = original_write with looking_glass() as what: print('lijiachang') print(what) print(what) print('back to normal')
知识点 :
- yield 后面的值会绑定到with语句中as子句的目标变量上,执行with块中的代码,这个函数会在这里暂停。
- 控制权一旦调成with块,就会继续执行yeild语句后的代码
@contextmanager 原理和注意事项
其实,contextlib.contextmanager装饰器会把函数包装实现成__enter__和__exit__方法的类。(ps:类的名字叫_GeneratorContextManager)
这个类的__enter__方法有如下作用:
- 调用生成器函数,保存生成器对象(这里把他称为gen)。
- 调用next(gen),执行到yeild关键字所在的位置。
- 返回next(gen)产出的值,把产出的值绑定到with/as语句的目标变量上。
with块终止时,__exit__方法会做以下事情:
- 检查有没有异常传给exc_type:
- 如果有,就调用gen.throw(exception), 在生成器函数定义体中包含yeild关键字的那一行抛出异常。
- 如果没有异常,再次调用next(gen),继续执行定义体中yeild语句之后的代码。
在上面的示例中,有一个严重的问题:如果在with块中抛出了异常,Python解释器会捕获,然后在looking_glass函数的yeild表达式再次抛出。但是问题是没有处理错误的代码,那么looking_glass函数就会终止,永远的无法恢复成sys.stdout.write方法原始个功能,导致系统的输出处于无效状态。
所以要添加一下异常的处理,比如ZeroDivisionError异常。
示例,添加异常处理的基于生成器的上下文管理器
import contextlib @contextlib.contextmanager def looking_glass(): """镜子:看到的字是反的""" import sys original_write = sys.stdout.write # 把原始的[屏幕打印输出]函数保存到一个实例属性中,供以后使用 def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write # 猴子补丁:替换成自己的方法实现 msg = '' try: yield "ABCD" # 只需要捕获yield部分 except ZeroDivisionError: msg = 'do not divide by zero' finally: sys.stdout.write = original_write if msg: print(msg) with looking_glass() as what: 0 / 0 # 抛出异常 print('lijiachang') print(what) print(what) print('back to normal')
打印
do not divide by zero
ABCD
back to normal
知识点:
- 在生成器函数中只需要捕获yeild关键字这行的异常,Python解释器会在with块中的异常,转移到yield这行抛出。
- 所以在使用@contextmanager装饰器时,要把yeild语句放到try/finally语句中,因为我们永远不知道上下文管理器的用户会在with块中做什么。
关于异常的处理的对比:
- 在用类实现上下文管理器时,前面说过,为了告诉解释器异常已经处理过了,需要在__exit__方法中返回True,此时解释器会压制异常。如果__exit__没有显示的返回一个值,那么解释器得到的就是None,此时会向上冒泡异常。
- 在用@contextmanager装饰器时,默认的行为是相反的:装饰器提供的__exit__方法假定发给生成器的所有异常都已经处理了,因此默认压制异常。如果不想让contextmanager压制异常,必须在装饰的函数中显式的重新抛出异常。
最后,再次强调:在@contextmanager装饰器装饰去生成器中,yield与迭代没有任何关系。
加载全部内容