概述
目录
一、绪论
二、基本概念
三、简单示例
四、原理阐述
五、基于类实现上下文管理器 —— 自定义实例与说明
5.1 定义前提
5.2 简单实现
5.3 异常处理
六、基于生成器实现上下文管理器
相关文章:【Python】详解 try-except-else-finally 语句 —— 异常处理完全解读 (上)
一、绪论
在《【Python】详解 try-except-else-finally 语句 —— 异常处理完全解读 (上) 》一文中,全面地讲解了 Python 处理异常的基本手段、机制与应用。本文将承接上文,详细说明另一种处理方式,即用于 上下文管理器 (Context manager) 的 with 语句。
二、基本概念
首先,开门见山、先入为主地给出上下文管理器与 with 语句的相关基本概念:
- 上下文管理协议 (Context management protocol):包含 __enter__() 和 __exit__() 方法,支持该协议的对象要求必须实现这两个方法,以成为上下文管理器。
- 上下文管理器 (Context manager):支持上下文管理协议 —— 实现了 __enter__() 和 __exit__() 方法的对象。上下文管理器用于 定义执行 with 语句时要建立的运行时上下文,并负责执行 with 语句块 上下文的进入与退出 操作。通常使用 with 语句调用上下文管理器,也可通过直接调用其方法来使用。
- 运行时上下文 (Runtime context):由上下文管理器创建、通过 __enter__() 和 __exit__() 方法实现进入和退出。其中,__enter__() 方法在语句体执行开始前进入运行时上下文,__exit__() 在语句体执行结束后退出运行时上下文。with 语句支持“运行时上下文”这一概念。
- 上下文表达式 (Context expression):with 语句中跟在关键字 with 后的表达式。该表达式返回一个上下文管理器对象。
- 语句体 (with-body):即 with 语句中的代码块。执行语句体前,会调用上下文管理器的 __enter__() 方法;执行语句体后,会调用上下文管理器 __exit__() 方法。
上下文管理器常用于资源的精确分配和释放,且常使用 with 语句调用 (说明还有其他方式)。
上述概念虽然较严谨而晦涩,但尚不必畏难。后续将先展示一个简单例子,再给出原理定义,后作出实例分析,且重要内容会强调多次,从而促进理解和领悟。(上述概念需要反复回头看理解)
三、简单示例
假如现在要打开一个文件,并写入一条数据,然后关闭之。那么,常见的 try- finally 语句实现如下:
>>> file = open('test.txt', 'w')
# 文件对象
>>> try:
file.write('Hello!')
# 写入
finally:
file.close()
# 关闭
这样写固然没错,但其实存在另一种等价写法 —— with 语句:
>>> with open('test.txt', 'w') as file:
file.write('Hello!')
可见,使用 with 语句,不但简化了许多样板代码 (boilerplate code),而且确保⽂件会被关闭,而无须关心如何退出众多子句构成的代码块。
以上便是上下文管理器实现资源加锁/解锁和关闭已打开文件的一个常见用例,是异常处理的“强化版本”。接下来,将说明这背后的原理。
四、原理阐述
Python 对一些内建对象进行改进,加入了对上下文管理器的支持。上下文管理器的典型用途包括:保存和恢复各种全局状态、锁定和解锁资源、关闭已打开的文件 等。
with 语句则用于包装带有使用上下文管理器定义方法的代码块的执行,从而允许对普通的 try-except-finally 语句使用一种模式封装以方便使用。
with 语句的语法格式为:
with context_expression [as target(s)]:
with-body
with 语句的执行过程由代码实现说明如下:
context_manager = context_expression
# 由上下文表达式返回一个上下文管理器对象
exit = type(context_manager).__exit__
# 载入上下文管理器的 __exit__() 方法
value = type(context_manager).__enter__(context_manager)
# 发起调用上下文管理器的 __enter__() 方法
exc = True
# True 表示正常执行, 即便有异常也忽略;False 表示重新抛出异常, 需对异常处理
try:
try:
target = value
# 若指定 as 子句 (赋值目标变量 traget(x))
with-body
# 执行 with-body (with 语句体/代码块)
except:
exc = False
# 执行过程中有异常发生
# 若 __exit__ 返回 True, 则异常被忽略;若 __exit__ 返回 False, 则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常执行完毕退出
# 或通过 statement-body 中的 break/continue/return 语句退出
# 或忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None, None 在 bool 上下文中看做是 False (bool(None)=False)
with 语句的执行过程文字说明如下:
- 对上下文表达式 (context_expression) 求值以生成上下文管理器 (context_manager)。
- 载入(保存)上下文管理器的 __exit__() 方法以便后续使用。
- 调用上下文管理器的 __enter__() 方法,进入关联到此对象的运行时上下文 (runtime context)。
- 若 with 语句指定了 as 子句,则上下文管理器 __enter__() 方法的返回值将赋给目标变量 target(s)。target(s) 既可为单变量,也可为包含多元素的 tuple (必须由圆括号 () 包裹)。注意,with 语句会保证若 __enter__() 方法返回时未发生错误的情况下,__exit__() 方法总能被调用。 因此,若在对目标变量(或 tuple) 赋值期间发生错误,则会将其视为在 with 语句体内部发生的错误,总之详见第 6 步。
- 执行 with 语句体/代码块。
- 不论如何,总会调用上下文管理器的 __exit__() 方法,退出关联到此对象的运行时上下文 (runtime context),而__exit__() 方法通常负责执行“清理”工作,如释放资源等。
- 注意,若由异常导致 with 语句体的退出,则将使用 sys.exc_info 获取异常信息 —— 类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为参数,调用 __exit__(exc_type, exc_value, exc_traceback) 方法;
- 若由异常导致 with 语句体的退出,且 __exit__() 方法的返回值为 False,则该异常会被重新引发;若 __exit__() 方法的返回值为 True,则该异常会被抑制/屏蔽,并将继续执行 with 语句后的语句;
- 若执行过程中未出现异常,或 with 语句体中执行了 break / continue / return 语句,则将以 3 个 None 作为参数调用 __exit__(None, None, None) 方法;
- 若由异常之外的任何原因导致 with 语句体的退出,则 __exit__() 方法的返回值将被忽略,并在该类退出正常的发生位置继续执行。
以上是较为严谨但却晦涩的原理概述,接下来将结合实例进行说明。
五、基于类实现上下文管理器 —— 自定义实例与说明
5.1 定义前提
自定义上下文管理器类 (即自定义支持上下文管理协议的类),至少要实现 __enter__() 和 __exit__() 方法。因为 with 语句总会先检查是否提供 __exit__() 方法,后检查是否定义 __enter__() 方法,二者任缺其一都会导致 AttributeError。
5.2 简单实现
接下来将构造一个上下文管理器类 File,例如:
>>> class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()
由于定义了 __enter__() 和 __exit__() 方法支持上下文管理协议,File 类成为了一个上下文管理器类,因此可在 with 语句中调用它:
>>> with File('test.txt', 'w') as file:
file.write('World!')
表面上看,with 语句完成的操作是:读取 test.txt 文件并写入 World! 。
实质上,内部进行的步骤是:with 语句先暂存 File 类的 __exit__ 方法,然后调用 File 类的 __enter__ 方法 (进入上下文管理器的运行时上下文)。 __enter__ 方法打开文件 test.txt 并返回给 with 语句,而打开的文件句柄被传递给变量 file (将文件对象赋值给变量 file)。接着,执行 with 语句体/代码块,令 file 调用 write() 方法在文件 test.txt 中写入 World! 。最后,with 语句调用先前暂存的 __exit__ 方法 (退出上下文管理器的运行时上下文),正常关闭文件 test.txt 。
5.3 异常处理
其实,__enter__ 方法含义简单且无参数,其原型为:
- context_manager.__enter__():进入上下文管理器的运行时上下文,在语句体执行前调用。若指定了 as 子句,with 语句将该方法的返回值赋值给 as 子句中的 target(s)。
而 __eixt__ 方法则复杂些,有三个形参 exc_type, exc_value, exc_traceback,其原型为:
- context_manager.__exit__(exc_type, exc_value, exc_traceback):退出与上下文管理器相关的运行时上下文,返回一个bool 值表示是否对发生的异常进行处理。参数为引起退出操作的异常信息,如果退出时未发生异常,则3个参数均为None。若发生异常,返回 True 表示无需处理异常,返回 False 则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。若该方法内部产生异常,则会取代由 statement-body 中语句产生的异常。要处理异常时,不应显示重新抛出异常 (即不能重新抛出通过参数传递进来的异常),只需将返回值设为 False 即可。之后,上下文管理代码会检测是否 __exit__() 失败来处理异常。
在 5.2 节内部步骤中,“打开的文件句柄传递”与“with 调用 __exit__ 方法”之间,若发生异常,Python 将使用 sys.exc_info 获取异常信息,将异常类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为实参,调用 __exit__(exc_type, exc_value, exc_traceback) 方法,从而让 __exit__ 方法决定如何关闭文件以及是否需要其他步骤。
事实上,在访问文件对象时,很多情况都可能导致异常发生。例如,调用一个不支持 (因为不存在) 的文件对象方法:
>>> with File('test.txt', 'w') as file:
file.undefinded_function('Bye!')
# 未定义 undefinded_function()
Traceback (most recent call last):
File "<pyshell#25>", line 2, in <module>
file.undefinded_function('Bye!')
AttributeError: '_io.TextIOWrapper' object has no attribute 'undefinded_function'
当异常发生时,with 语句会采取如下措施:
- 将发生异常的类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为实参传递给 __exit__ 方法;
- 令 __exit__ 方法处理异常;
- 若 __exit__ 方法返回 True,则说明该异常已被“处理”,无需再处理;
- 若 __exit__ 方法返回 True 之外的任何内容,则该异常将被 with 语句抛出;
在 5.2 节的例子 —— File 类中,__exit__ 方法返回 None (若无显式定义的 return 语句,方法默认返回 None)。因此,5.3 节的上例在调用不存在的 undefinded_function 方法时,抛出了异常。
为此,可以尝试完善 __exit__ 方法以处理异常:
>>> class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
print("Exception has been handled")
# 提示语
self.file_obj.close()
# 关闭文件对象
print("File has been closed")
# 提示语
return True
# 返回 True 说明异常已被处理
这时,再试图触发异常:
>>> with File('test.txt', 'w') as file:
file.undefinded_function('Bye!')
Exception has been handled
File has been closed
完全按照预期发生!因为自定义的 __exit__ 方法返回了 True,所有没有异常会被 with 语句抛出。
同时,提示语 “File has been closed” 的输出也说明了发生异常时,with 语句体/代码块中并未执行完,但保证会将文件关闭 (确保资源被释放掉),从而体现了一定的安全性。因此,可以自定义上下文管理器来对软件系统中的资源进行管理,比如数据库连接、共享资源的访问控制等
此外,如果有多个上下文管理器,则会视为存在多个语句嵌套处理。即:
>>> with CM1 as cm1, CM2 as cm2:
suite
# --------------------------------- 等价于 ----------------------------------
>>> with CM1 as cm1:
with CM2 as cm2:
suite
不过通常不会这样复杂地嵌套使用,因此仅作展示不作赘述。
六、基于生成器实现上下文管理器
事实上,除了自定义类,还可以基于装饰器(decorators) 和生成器(generators) 实现上下文管理器。
Python 中的 contextlib 模块正用于此。contextlib 模块提供了3个对象:装饰器 contextmanager、函数 nested 和上下文管理器 closing。使用这些对象,可包装已有生成器函数或对象,从而加入对上下文管理协议的支持,避免了专门编写上下文管理器来支持 with 语句。
然而,contextlib 模块平时似乎用得并不多,因此点到为止,不再赘述。有兴趣可以看一下 contextlib 模块的文档。
参考文献:
《Python Immediate》、
https://docs.python.org/zh-cn/3.6/tutorial/errors.html?highlight=异常
3. 数据模型 — Python 3.6.15 文档
8. 复合语句 — Python 3.6.15 文档
IBM Developer
29.6. contextlib — Utilities for with-statement contexts — Python 3.6.15 文档
最后
以上就是爱撒娇云朵为你收集整理的【Python】详解 with 语句 (上下文管理器) —— 异常处理完全解读 (下)一、绪论二、基本概念三、简单示例四、原理阐述五、基于类实现上下文管理器 —— 自定义实例与说明六、基于生成器实现上下文管理器的全部内容,希望文章能够帮你解决【Python】详解 with 语句 (上下文管理器) —— 异常处理完全解读 (下)一、绪论二、基本概念三、简单示例四、原理阐述五、基于类实现上下文管理器 —— 自定义实例与说明六、基于生成器实现上下文管理器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复