Python with 语句,with 语句上下文管理器介绍
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 部分
block
为with
语句的主体代码,需要使用某种空白字符进行缩进,以表示其归属于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
方法将引发异常。
# 文件对象的 __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
语句上下文管理器CM
,CM
的__enter__
方法用于保存模块变量students
中的列表,CM
的__exit__
方法用于恢复模块变量students
中的列表。
# 模块变量 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
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
语句包含了两个上下文管理器,我们姑且称他们为A
和B
。对于__enter__
方法,将按照A
,B
的顺序被调用,对于__exit__
方法,将按照B
,A
的顺序被调用。
# 自定义 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
,异常将按照B
,A
的顺序传播。
# …
# 异常将按照 B,A 的顺序传播
with CM('A'), CM('B'):
num = 1 / 0
__exit__ B <class 'ZeroDivisionError'>
__exit__ A <class 'ZeroDivisionError'>
…
ZeroDivisionError: division by zero