在工作当中写代码的时候遇到当需要使用python打开文件的时候,程序会提示你使用with语句而是不是open+close,以前自己都未曾关心为啥,这样做有什么优势,现在越发好奇了。

With语句是什么?

最常见的解释就是说当年你需要打开一个文件的时候,如果只是打开不关闭是危险或者不规范的操作,所以必须要有打开也要有关闭,如下代码:

1
2
3
file = open("test.txt")
data = file.read()
file.close()

但是这里有两个问题:

  • 一是可能忘记关闭文件句柄;
  • 二是文件读取数据发生异常,没有进行任何处理。

所以为了安全起见推荐有以下改进方法:

1
2
3
4
5
file = open("test.txt")
try:
data = file.read()
finally:
file.close()

虽然更加安全,但是冗长以及理解更困难的问题也随之出现了。而with语法正好可以解决这个问题:

1
2
with open("/tmp/foo.txt") as file:
data = file.read()

上下文管理器

with语句的使用被成为上下文管理器。当与第一个例子对比时,可以看到,通过使用 with,许多样板代码(boilerplate code)被消掉了。 这就是 with 语句的主要优势,它确保文件会被关闭,而不用关注嵌套代码如何退出。上下文管理器的一个常见用例,是资源的加锁和解锁,以及关闭已打开的文件(就像我已经展示给你看的)。

以下展示关于上下文管理器的实现代码:

基于类

一个上下文管理器的类,最起码要定义 __enter____exit__ 方法。

1
2
3
4
5
6
7
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__ 方法,可以在with语句里使用它

1
2
with File('demo.txt', 'w') as opened_file:
opened_file.write('Hola!')

__exit__ 函数接受三个参数。这些参数对于每个上下文管理器类中的 __exit__ 方法都是必须的。

  • with 语句先暂存了 File 类的 __exit__ 方法。
  • 然后它调用 File 类的 __enter__ 方法。
  • __enter__ 方法打开文件并返回给 with 语句。
  • 打开的文件句柄被传递给 opened_file 参数。
  • 使用 .write() 来写文件。
  • with 语句调用之前暂存的 __exit__ 方法。
  • __exit__ 方法关闭了文件。

__exit__ 方法的这三个参数:typevaluetraceback。 在第4步和第6步之间,如果发生异常,Python 会将异常的 typevaluetraceback 传递给 __exit__ 方法。 它让 __exit__ 方法来决定如何关闭文件以及是否需要其他步骤。在案例中,并没有注意它们。
那如果文件对象抛出一个异常呢?万一尝试访问文件对象的一个不支持的方法。举个例子:

1
2
with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function('Hola!')

来列一下,当异常发生时,with 语句会采取哪些步骤。

  • 它把异常的 typevaluetraceback 传递给 __exit__方法。
  • 它让 __exit__ 方法来处理异常。
  • 如果 __exit__ 返回的是 True,那么这个异常就被优雅地处理了。
  • 如果 __exit__ 返回的是 True 以外的任何东西,那么这个异常将被 with 语句抛出。

案例中,__exit__ 方法返回的是 None (如果没有 return 语句那么方法会返回 None)。因此,with 语句抛出了那个异常。

1
2
3
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'file' object has no attribute 'undefined_function'

尝试下在 __exit__ 方法中处理异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
return True

with File('demo.txt', 'w') as opened_file:
opened_file.undefined_function()

# Output: Exception has been handled

__exit__ 方法返回了 True,因此没有异常会被 with 语句抛出。