Python with 语句,with 语句上下文管理器介绍

我被代码海扁署名-非商业-禁演绎
阅读 9:03·字数 2717·发布 
Bilibili 空间
关注 960

Python with 语句

如果你希望能够及时释放重要的资源,以防止其因为某些情况(比如异常)而始终处于被占用的状态,或者希望临时保存当前状态以便在稍微恢复,那么 Python 提供的with语句可用于完成上述目标,该语句仅接受with语句上下文管理器作为处理目标,其基本书写格式如下。

with <expression>[ as <target>]:
    <block>

expression 部分

expression是一个运算结果为with语句上下文管理器的表达式,比如,一个调用 Python 内置函数open获得文件对象的表达式(Python 文件对象是有效的with语句上下文管理器)。

target 部分

target是变量的名称,该变量将存储with语句上下文管理器(即expression部分的运算结果)的__enter__方法的返回值。变量名称需要符合 Python 的标识符规范,不能使用 Python 关键字或保留关键字。

block 部分

blockwith语句的主体代码,需要使用某种空白字符进行缩进,以表示其归属于with语句。

在 Pythonwith语句开始执行时,expression部分所返回的with语句上下文管理器的__enter__方法将被调用,你可以在该方法中保存一些重要的数据或状态。

在 Pythonwith语句结束执行时,with语句上下文管理器的__exit__方法将被调用,这里所说的语句结束包括正常结束和非正常结束(比如,block部分的代码出现了异常)。由于,Pythonwith语句的上下文管理器的__exit__方法总是被调用,因此__exit__方法可用于释放一些重要的资源,或恢复之前保存的数据或状态。

Python with 语句的上下文管理器的 __enter__ 方法可以返回自身

Python 的with语句对上下文管理器的__enter__方法的返回值的类型没有要求,因此,__enter__方法可以返回上下文管理器自身,以方便代码的编写和阅读。

下面,我们使用with语句来打开文件with.txt,由于 Python 文件对象的__enter__方法返回其自身,因此变量file指向文件对象,可以使用他将内容写入文件。

with语句结束时,Python 文件对象的__exit__方法会被调用,这将使文件对象被关闭,再次调用其write方法将引发异常。

with.py
# 文件对象的 __enter__ 方法返回自身
with open('with.txt', 'w', encoding='utf8') as file:
	file.write('with 语句')

# ERROR 文件对象已经被关闭 file.write('又一条 with 语句')
ValueError: I/O operation on closed file.

Python with 语句的上下文管理器

Pythonwith语句的上下文管理器应该拥有__enter____exit__方法,如前所述,他们分别在with语句开始和结束时被调用,__enter__方法的返回值将作为with…as语句所定义的变量的值。

__enter__(self)

如果 Pythonwith语句的主体代码引发了异常并且没有处理,那么异常的信息将被传递给with语句上下文管理器的__exit__方法的三个参数,如果此时__exit__方法返回True或被视为True的值,那么异常将被抑制,后续代码可以在不处理异常的情况下继续运行。

如果 Pythonwith语句的主体代码没有引发异常或者异常得到了处理,那么with语句上下文管理器的__exit__方法的三个参数将为None

__exit__(self, exc_type, exc_value, exc_tb)

exc_type 参数

exc_type参数是一个type对象,表示with语句的主体代码所引发的异常的类型。

exc_value 参数

exc_value参数为with语句的主体代码所引发的异常。

exc_tb 参数

exc_tb参数表示with语句的主体代码所引发的异常的回溯类型。

不需要在 Python with 语句的上下文管理器的 __exit__ 方法中再次抛出异常

当 Pythonwith语句的主体代码引发了异常且异常没有被处理时,不需要在with语句的上下文管理器的__exit__方法中再次抛出异常,只要确保__exit__方法没有返回True或被视为True的值,异常便可继续传播,即with语句以外的代码应负责处理异常。

下面,我们定义了自己的with语句上下文管理器CMCM__enter__方法用于保存模块变量students中的列表,CM__exit__方法用于恢复模块变量students中的列表。

with_cm.py
# 模块变量 students
students = ['小红', '小黑']

# 自定义 with 语句上下文管理器 CM class CM: def __enter__(self): # 将模块变量 students 中的列表保存到 CM 中 global students self.students = students
# 将新的列表赋值给模块变量 students students = ['小兰', '小白'] return students
def __exit__(self, exc_type, exc_value, exc_tb): # 恢复模块变量 students 中原有的列表 global students students = self.students
# CM 会临时改变模块变量 students with CM() as s: print(s, students)
print(students)
['小兰', '小白'] ['小兰', '小白']
['小红', '小黑']

在下面的示例中,with语句引发了异常ZeroDivisionError,该异常的相关信息被传递给上下文管理器对象CM__exit__方法,由于__exit__方法返回了True,因此异常ZeroDivisionError将被抑制。

with_cm_exc.py
# 自定义 with 语句上下文管理器 CM
class CM:
	def __enter__(self):
		pass
	def __exit__(self, exc_type, exc_value, exc_tb):
		print(exc_type, exc_value, exc_tb)
		# 返回 True 后,异常将被抑制
		return True

with CM(): # 异常 ZeroDivisionError 被引发并且没有得到处理 num = 1 / 0
<class 'ZeroDivisionError'> division by zero <traceback object at >

在 Python with 语句中包含多个上下文管理器

一般情况下,Pythonwith语句只处理一个上下文管理器,但如果有需要,你可以在with语句中包含多个上下文管理器,并使用逗号,分隔表达式,比如expression1 as target1,expression2 as target2。当然,你还可以使用括号()括住所有 Pythonwith语句上下文管理器对应的表达式,这样每一个表达式可单独占用一行。需要指出,这里的括号()并不会创建 Python 元组。

如果 Pythonwith语句包含了多个上下文管理器,那么将按照顺序依次调用他们的__enter__方法,并按照相反的顺序调用他们的__exit__方法。

在下面的示例中,with语句包含了两个上下文管理器,我们姑且称他们为AB。对于__enter__方法,将按照AB的顺序被调用,对于__exit__方法,将按照BA的顺序被调用。

with_cm_multi.py
# 自定义 with 语句上下文管理器 CM
class CM:
	# CM 的构造器
	def __init__(self, name):
		self.name = name

# __enter__ 和 __exit__ 方法将显示 CM 的名称 def __enter__(self): print(f'__enter__ {self.name}') def __exit__(self, exc_type, exc_value, exc_tb): print(f'__exit__ {self.name} {exc_type}')
# 包含了两个上下文管理器 with ( CM('A'), CM('B') ): pass
__enter__ A
__enter__ B
__exit__ B None
__exit__ A None

异常会按照从后向前的顺序在 Python with 语句的多个上下文管理器中传播

除非异常被处理或被__exit__方法抑制,否则他将按照从后向前的顺序在 Pythonwith语句的多个上下文管理器中传播。

我们为上面的示例增加一些代码,在with语句中引发异常ZeroDivisionError,异常将按照BA的顺序传播。

with_cm_multi.py
# …
# 异常将按照 B,A 的顺序传播
with CM('A'), CM('B'):
	num = 1 / 0
__exit__ B <class 'ZeroDivisionError'>
__exit__ A <class 'ZeroDivisionError'>

ZeroDivisionError: division by zero

源码

src/zh/statements/with·codebeatme/python·GitHub