【Python】详解 with 语句 (上下文管理器) —— 异常处理完全解读 (下)

目录

一、绪论

二、基本概念

三、简单示例

四、原理阐述

五、基于类实现上下文管理器 —— 自定义实例与说明

5.1 定义前提

5.2 简单实现

5.3 异常处理

六、基于生成器实现上下文管理器


相关文章:【Python】详解 try-except-else-finally 语句 —— 异常处理完全解读 (上) 


一、绪论

在《【Python】详解 try-except-else-finally 语句 —— 异常处理完全解读 (上) 》一文中,全面地讲解了 Python 处理异常的基本手段、机制与应用。本文将承接上文,详细说明另一种处理方式,即用于 上下文管理器 (Context manager) with 语句


二、基本概念

首先,开门见山、先入为主地给出上下文管理器与 with 语句的相关基本概念:

  1. 上下文管理协议 (Context management protocol):包含 __enter__() 和 __exit__() 方法,支持该协议的对象要求必须实现这两个方法,以成为上下文管理器。
  2. 上下文管理器 (Context manager):支持上下文管理协议 —— 实现了 __enter__() 和 __exit__() 方法的对象。上下文管理器用于 定义执行 with 语句时要建立的运行时上下文,并负责执行 with 语句块 上下文的进入与退出 操作。通常使用 with 语句调用上下文管理器,也可通过直接调用其方法来使用。
  3. 运行时上下文 (Runtime context):由上下文管理器创建、通过 __enter__() 和 __exit__() 方法实现进入和退出。其中,__enter__() 方法在语句体执行开始前进入运行时上下文,__exit__() 在语句体执行结束后退出运行时上下文。with 语句支持“运行时上下文”这一概念。
  4. 上下文表达式 (Context expression):with 语句中跟在关键字 with 后的表达式。该表达式返回一个上下文管理器对象。
  5. 语句体 (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 语句的执行过程文字说明如下:

  1. 上下文表达式 (context_expression) 求值以生成上下文管理器 (context_manager)
  2. 载入(保存)上下文管理器的 __exit__() 方法以便后续使用。
  3. 调用上下文管理器的 __enter__() 方法,进入关联到此对象的运行时上下文 (runtime context)
  4. 若 with 语句指定了 as 子句,则上下文管理器 __enter__() 方法的返回值将赋给目标变量 target(s)。target(s) 既可为单变量,也可为包含多元素的 tuple (必须由圆括号 () 包裹)。注意,with 语句会保证若 __enter__() 方法返回时未发生错误的情况下,__exit__() 方法总能被调用。 因此,若在对目标变量(或 tuple) 赋值期间发生错误,则会将其视为在 with 语句体内部发生的错误,总之详见第 6 步。
  5. 执行 with 语句体/代码块。
  6. 不论如何,总会调用上下文管理器的 __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 语句会采取如下措施:

  1. 将发生异常的类型 (exc_type)、值 (exc_value) 及回溯信息 (traceback) 作为实参传递给 __exit__ 方法;
  2. 令 __exit__ 方法处理异常;
  3. 若 __exit__ 方法返回 True,则说明该异常已被“处理”,无需再处理;
  4. 若 __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 文档

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值