分析用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 相关文章推荐
编写自定义的Django模板加载器的简单示例
Jul 21 Python
python用reduce和map把字符串转为数字的方法
Dec 19 Python
Django使用httpresponse返回用户头像实例代码
Jan 26 Python
python学生信息管理系统(完整版)
Apr 05 Python
python numpy 按行归一化的实例
Jan 21 Python
python绘制多个子图的实例
Jul 07 Python
pytorch 固定部分参数训练的方法
Aug 17 Python
Python中注释(多行注释和单行注释)的用法实例
Aug 28 Python
关于python 跨域处理方式详解
Mar 28 Python
python pyecharts 实现一个文件绘制多张图
May 13 Python
python能做哪方面的工作
Jun 15 Python
基于matplotlib中ion()和ioff()的使用详解
Jun 16 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 fsockopen解决办法 php实现多线程
2014/01/20 PHP
PHP使用get_headers函数判断远程文件是否存在的方法
2014/11/28 PHP
Joomla使用Apache重写模式的方法
2016/05/04 PHP
PHP+Jquery与ajax相结合实现下拉淡出瀑布流效果【无需插件】
2016/05/06 PHP
javascript 复杂的嵌套环境中输出单引号和双引号
2009/05/26 Javascript
jQuery最佳实践完整篇
2011/08/20 Javascript
JQuery实现倒计时按钮具体方法
2013/11/14 Javascript
Javascript window对象详解
2014/11/12 Javascript
浅谈JavaScript中null和undefined
2015/07/09 Javascript
理解javascript中的原型和原型链
2015/07/30 Javascript
EasyUI在表单提交之前进行验证的实例代码
2016/06/24 Javascript
JavaScript中this的用法及this在不同应用场景的作用解析
2017/04/13 Javascript
React-router中结合webpack实现按需加载实例
2017/05/25 Javascript
vue-cli V3.0版本的使用详解
2018/10/24 Javascript
基于javascript实现放大镜特效
2020/12/03 Javascript
微信小程序组件生命周期的踩坑记录
2021/03/03 Javascript
[01:56]2014DOTA2西雅图邀请赛 MVP外卡赛老队长精辟点评
2014/07/09 DOTA
Python数据结构与算法之列表(链表,linked list)简单实现
2017/10/30 Python
Python中XlsxWriter模块简介与用法分析
2018/04/24 Python
python生成器与迭代器详解
2019/01/01 Python
解决python文件双击运行秒退的问题
2019/06/24 Python
PyTorch中的Variable变量详解
2020/01/07 Python
python3访问字典里的值实例方法
2020/11/18 Python
python绘制高斯曲线
2021/02/19 Python
HTML5 Canvas像素处理使用接口介绍
2012/12/02 HTML / CSS
ProBikeKit澳大利亚:自行车套件,跑步和铁人三项装备
2016/11/30 全球购物
美国最流行的男士时尚网站:Touch of Modern
2018/02/05 全球购物
英国领先的大码时装品牌之一:Elvi
2018/08/26 全球购物
Hobbs官方网站:英国奢华女性时尚服装
2020/02/22 全球购物
家长给孩子的评语
2014/01/30 职场文书
热爱祖国的演讲稿
2014/05/04 职场文书
社区维稳工作方案
2014/06/06 职场文书
班子成员四风问题自我剖析材料
2014/09/29 职场文书
致青春观后感
2015/06/09 职场文书
行政复议决定书
2015/06/24 职场文书
总结Python变量的相关知识
2021/06/28 Python