Python上下文管理器详细使用教程
作者:lijiachang8
with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下问题。
这样做能避免错误并减少样板代码,因此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与迭代没有任何关系。
到此这篇关于Python上下文管理器详细使用教程的文章就介绍到这了,更多相关Python上下文管理器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!