Python上下文管理器详细使用教程
作者:lijiachang8 发布时间:2021-06-24 05:47:35
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与迭代没有任何关系。
来源:https://blog.csdn.net/lijiachang8/article/details/128825353


猜你喜欢
- 一、绘制总体图形import numpy as npimport matplotlib.pyplot as pltfrom mpl_tool
- 本文实例讲述了javascript基于prototype实现类似OOP继承的方法。分享给大家供大家参考,具体如下:这里要说明的是,公有属性(
- 场景描述在update表的时候出现DeadlockLoserDataAccessException异常 (Deadlock found wh
- 搭建MySQL主从后,很多时候不知道从的状态是否ok,有时候出现异常不能及时知道,这里通过shell脚本结合zabbix实现监控并告警一般情
- 解读model.named_parameters()与model.parameters()model.named_parameters()迭
- 随着网络的迅速发展 发展 发展,二维码的应用将会越来越多。同时很多只是很平凡的二维码,请拿起你的手 把这个二维码 设计起来吧。下面分享了几个
- 上一次,我们谈到在ASP中如何利用“正则表达式”对象来实现各种数据的校验,文中描述了正则表达式对象的强大功能,接下来,我们来看看有关“正则表
- 效果如下,dialog中内容自行添加<template> <div> <div class="dia
- 为了减少页面的加载速度,提高用户体验,对于一些图片决定使用图标代替,但是发现element-ui的图标少得可怜,完全满足不了我的要求,于是决
- 1.1.1 摘要 Join是关系型数据库系统的重要操作之一,SQL Server中包含的常用Join:内联接、外联接和交叉联接等。如果我们想
- 前言1.本文使用的是mysql8.0版本与5.0版本相比:导包方式相同,后面代码中的注册驱动方式不同1.mac与pc的idea菜单和图标不是
- 二分类问题可能是应用最广泛的机器学习问题。今天我们将学习根据电影评论的文字内容将其划分为正面或负面。一、数据集来源我们使用的是IMDB数据集
- 1. 引言在Python中我们经常使用pip来安装第三方Python软件包,其实我们每个人都可以免费地将自己写的Python包发布到PyPI
- 如下所示://定义编码 header( 'Content-Type:text/html;charset=utf-8 &#
- 编程是数据科学中不可或缺的技能,虽然创建脚本来执行基本功能很容易,但编写大规模可读性良好的代码需要更多的思考。关于PEP-8pycodest
- 想跟大家聊聊关于 mysql 中的两个小的知识点:redo log 和 binlog 。redo log :InnoDB 存储引擎层方面的日
- 前言近期在工作中遇到某表某字段是可扩展数据内容,信息以逗号分隔生成的,现需求要根据此字段数据在其它表查询相关的内容展现出来,第一想法是切割数
- 读取问题如下所示,我们在文本中写了一个问题,然后将其读取出来。“黄河远上白云间,一片孤城万仞山。”的作者是谁?王之涣李白白居易杜甫file
- 在ORACLE中,我们可以通过file_id(file#)与block_id(block#)去定位一个数据库对象(object)。例如,我们
- 本文主要介绍了OpenCV 图像对比度,具有一定的参考价值,感兴趣的可以了解一下实现原理图像对比度指的是一幅图像中明暗区域最亮的白和最暗的黑