分析用Python脚本关闭文件操作的机制


Posted in Python onJune 28, 2015

如果不用“with”,那么Python会在何时关闭文件呢?答案是:视情况而定。

Python程序员最初学到的东西里有一点就是可以通过迭代法很容易地遍历一个打开文件的全文:

f = open('/etc/passwd')
for line in f:
  print(line)

注意上面的代码具有可行性,因为我们的文件对象“f”是一个迭代器。换句话说,“f“ 知道在一个循环或者任何其他的迭代上下文中做什么,比如像列表解析。

我的Python课堂上的大多数学生都具有其他编程语言背景,在使用以前所熟悉的语言时,他们总是在完成文件操作时被期望关闭文件。因此,在我向他们介绍了Python文件操作的内容不久后他们问起如何在Python中关闭文件时,我一点都不惊讶。

最简单的回答就是我们可以通过调用f.close()显式地关闭文件。一旦我们关闭了文件,该文件对象依然存在,但是我们无法再通过它来读取文件内容了,而且文件对象返回的可打印内容也表明文件已经被关闭。

>>> f = open('/etc/passwd')
>>> f
<open file '/etc/passwd', mode 'r' at 0x10f023270>
>>> f.read(5)
'##n# '
 
f.close()
>>> f
<closed file '/etc/passwd', mode 'r' at 0x10f023270>
 
f.read(5)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-11-ef8add6ff846> in <module>()
----> 1 f.read(5)
ValueError: I/O operation on closed file

所以是这样,我在用Python编程的时候,很少明确地对文件调用 “close” 方法。此外,你也很可能不想或不必那样做。

打开文件的优选最佳实践方式是使用 “with” 语句,就像如下所示:

with open('/etc/passwd') as f:
  for line in f:
    print(line)

“with”语句对 “f” 文件对象调用在Python中称作“上下文管理器”的方法。也就是说,它指定 “f” 为指向 /etc/passwd 内容的新的文件实例。在 “with” 打开的代码块内,文件是打开的,而且可以自由读取。

然而,一旦Python代码从 “with” 负责的代码段退出,文件会自动关闭。试图在我们退出 “with”代码块后从 f 中读取内容会导致和上文一样的 ValueError 异常。所以,通过使用 “with”,你避免了显式地关闭文件的操作。Python 会以一种不那么有 Python 风格的方式在幕后神奇而静静地替你关闭文件。

但是你不显式地关闭文件会怎样?如果你有点懒,既不使用 “with” 代码块也不调用f.close()怎么办?这时文件会什么时候关闭?何时应该关闭文件?

我之所以问这个,是因为我教了这么多年Python,确信努力教授“with”或上下文管理器的同时又教很多其它的话题超出了学生接受的范围。在介绍性课程谈及 “with” 时,我一般会告诉学生在他们职业生涯中遇到这个问题时,让Python去关闭文件就好,不论文件对象的应用计数降为0还是Python退出时。

在我的Python文件操作免费e-mail课程中,我并没有在所有的解决方案中使用with,想看看如何。结果一些人质疑我,说不使用“with”会向人们展示一种糟糕的实践方案并且会有数据未写入磁盘的风险。

我收到了很多关于此话题的邮件,于是我问自己:如果我们没有显式地关闭文件或者没用“with”代码块,那么Python会何时关闭文件?也就是说,如果我让文件自动关闭,那么会发生什么?

我总是假定当对象的引用计数降为0时,Python会关闭文件,进而垃圾回收机制清理文件对象。当我们读文件时很难证明或核实这一点,但写入文件时却很容易。这是因为当写入文件时,内容并不会立即刷新到磁盘(除非你向“open”方法的第三个可选参数传入“False”),只有当文件关闭时才会刷新。

于是我决定做些实验以便更好地理解Python到底能自动地为我做什么。我的实验包括打开一个文件、写入数据、删除引用和退出Python。我很好奇数据是什么时候会被写入,如果有的话。

我的实验是这个样子:

f = open('/tmp/output', 'w')
f.write('abcn')
f.write('defn')
# check contents of /tmp/output (1)
del(f)
# check contents of /tmp/output (2)
# exit from Python
# check contents of /tmp/output (3)

我在Mac平台上用Python 2.7.9 做了第一个实验,报告显示在阶段一文件存在但是是空的,阶段二和阶段三中文件包含所有的内容。这样,在CPython 2.7中我最初的直觉似乎是正确的:当一个文件对象被垃圾回收时,它的 __del__ (或者等价的)方法会刷新并关闭文件。而且在我的IPython进程中调用“lsof”命令显示文件确实在引用对象移除后被关闭了。

那 Python3 如何呢?我在Mac上 Python 3.4.2 环境下做了以上的实验,得到了相同的结果。移除对文件对象最后的引用后会导致文件被刷新并且被关闭。

这对于 Python 2.7 和 3.4 很好。但是在 PyPy 和 Jython下的替代实现会怎样呢?或许情况会有些不同。

于是我在 PyPy 2.7.8 下做了相同的实验。而这次,我得到了不同的结果!删除文件对象的引用后——也就是在阶段2,并没有导致文件内容被刷入磁盘。我不得不假设这和垃圾回收机制的不同或其他在 PyPy 和 CPython中工作机制的不同有关系。但是如果你在 PyPy中运行程序,就绝不要指望仅仅因为文件对象的引用结束,文件就会被刷新和关闭。命令 lsof 显示直到Python进程退出时文件才会被释放。

为了好玩,我决定尝试一下 Jython 2.7b3. 结果Jython 表现出了和PyPy一样的行为。也就是说,从 Python 退出确实会确保缓存中的数据写入磁盘。

我重做了这些实验,但是我把 “abcn”和 “defn”换成了 “abcn”*1000 和“defn”*1000.

在 Python 2.7 的环境下,“abcn” * 1000 语句执行后没有任何东西写入。但“defn” * 1000 语句执行后,文件包含有4096个字节——可能代表缓冲区的大小。调用 del(f) 删除文件对象的引用导致数据被刷入磁盘和文件关闭,此时文件中共有8000字节的数据。所以忽略字符串大小的话 Python 2.7 的行为表现基本相同。唯一不同的是如果超出了缓冲区的大小,那么一些数据将在最后文件关闭数据刷新前写入磁盘。

换做是Python 3的话,情况就有些不同了。f.write执行后没有任何数据会写入。但是文件对象引用一旦结束,文件就会刷新并关闭。这可能是缓冲区很大的缘故。但毫无疑问,删除文件对象引用会使文件刷新并关闭。

至于 PyPy 和 Jython,对大文件和小文件的操作结果都一样:文件在 PyPy 或 Jython 进程结束的时候刷新并关闭,而不是在文件对象的引用结束的时候。

为了再次确认,我又使用 “with” 进行了实验。在所有情况下,我们都能够轻松的预测文件是何时被刷新和关闭的——就是当退出代码段,并且上下文管理器在后台调用合适方法的时候。

换句话说,如果你不使用“with”,那么至少在非常简单的情形下,你的数据不一定有丢失的危险。然而你还是不能确定数据到底是在文件对象引用结束还是程序退出的时候被保存的。如果你假定因为对文件唯一的引用是一个本地变量所以文件在函数返回时会关闭,那么事实一定会让你感到吃惊。如果你有多个进程或线程同时对一个文件进行写操作,那么你真的要非常小心了。

或许这个行为可以更好地定义不就可以在不同的平台上表现得基本一致了吗?也许我们甚至可以看看Python规范的开始,而不是指着CPython说“Yeah,不管版本如何总是对的”。

我依然觉得“with”和上下文管理器很棒。而且我想对于Python新手,理解“with”的工作原理很难。但我还是不得不提醒新手开发者注意:如果他们决定使用Python的其他可选版本,那么会出现很多不同于CPython的古怪情况而且如果他们不够小心,甚至会深受其害。

Python 相关文章推荐
Python中zip()函数用法实例教程
Jul 31 Python
python 打印对象的所有属性值的方法
Sep 11 Python
Python编程scoketServer实现多线程同步实例代码
Jan 29 Python
python pycurl验证basic和digest认证的方法
May 02 Python
Python3.7实现中控考勤机自动连接
Aug 28 Python
Python叠加两幅栅格图像的实现方法
Jul 05 Python
PowerBI和Python关于数据分析的对比
Jul 11 Python
Numpy对数组的操作:创建、变形(升降维等)、计算、取值、复制、分割、合并
Aug 28 Python
Pycharm最新激活码2019(推荐)
Dec 31 Python
Python如何定义有默认参数的函数
Aug 10 Python
Django xadmin安装及使用详解
Oct 26 Python
Python Spyder 调出缩进对齐线的操作
Feb 26 Python
python实现linux下使用xcopy的方法
Jun 28 #Python
自动化Nginx服务器的反向代理的配置方法
Jun 28 #Python
python读取TXT到数组及列表去重后按原来顺序排序的方法
Jun 26 #Python
在Python中使用zlib模块进行数据压缩的教程
Jun 26 #Python
Python中的zipfile模块使用详解
Jun 25 #Python
Python实现保证只能运行一个脚本实例
Jun 24 #Python
Python中的默认参数详解
Jun 24 #Python
You might like
PHP HTML代码串截取代码
2008/12/29 PHP
php FPDF类库应用实现代码
2009/03/20 PHP
WordPress中&quot;无法将上传的文件移动至&quot;错误的解决方法
2015/07/01 PHP
php如何获取Http请求
2020/04/30 PHP
js自带函数备忘 数组
2006/12/29 Javascript
兼容Mozilla必须知道的知识。
2007/01/09 Javascript
jQuery AnythingSlider滑动效果插件
2010/02/07 Javascript
setTimeout和setInterval的区别你真的了解吗?
2011/03/31 Javascript
jquery中加载图片自适应大小主要实现代码
2013/08/23 Javascript
Jquery通过Ajax访问XML数据的小例子
2013/11/18 Javascript
js 设置缓存及获取设置的缓存
2014/05/08 Javascript
nodejs下打包模块archiver详解
2014/12/03 NodeJs
node.js中的http.response.setHeader方法使用说明
2014/12/14 Javascript
Bootstrap每天必学之js插件
2015/11/30 Javascript
JS克隆,属性,数组,对象,函数实例分析
2016/11/26 Javascript
Bootstrap选项卡学习笔记分享
2017/02/13 Javascript
详解Vue 开发模式下跨域问题
2017/06/06 Javascript
AngularJS 中的数据源的循环输出
2017/10/12 Javascript
JS生成随机打乱数组的方法示例
2017/12/23 Javascript
JavaScript实现简单的文本逐字打印效果示例
2018/04/12 Javascript
JavaScript实现指定数量的并发限制的示例代码
2020/03/10 Javascript
easyUI使用分页过滤器对数据进行分页操作实例分析
2020/06/01 Javascript
Vue绑定用户接口实现代码示例
2020/11/04 Javascript
JavaScript实现网页动态生成表格
2020/11/25 Javascript
Python聚类算法之凝聚层次聚类实例分析
2015/11/20 Python
Python3中类、模块、错误与异常、文件的简易教程
2017/11/20 Python
python构建深度神经网络(DNN)
2018/03/10 Python
Django使用Mysql数据库已经存在的数据表方法
2018/05/27 Python
Python使用Beautiful Soup爬取豆瓣音乐排行榜过程解析
2019/08/15 Python
python元组和字典的内建函数实例详解
2019/10/22 Python
python encrypt 实现AES加密的实例详解
2020/02/20 Python
Python CSS选择器爬取京东网商品信息过程解析
2020/06/01 Python
Python 创建守护进程的示例
2020/09/29 Python
基于Python爬取素材网站音频文件
2020/10/21 Python
授权委托书范本
2014/04/03 职场文书
2015年大学辅导员工作总结
2015/05/12 职场文书