https://segmentfault.com/q/1010000000397712 先驱们钻研的干货
我在路上就在想,这么底层的一个特性,为什么 Python 没有为程序员处理掉呢?都用 Python 了,谁会为了那么大的便利牺牲一丁点性能呢?一回来我就做了测试。
经测试,Python 2.7.5 on Windows XP 重现此情况,Python 3.3.2 on Windows XP 没有重现。这说明 Python 2 确实该换了!
再次更新:
找到原因之后我就觉得这问题在哪里见过,今天终于找出来了, python-cn 邮件列表里讨论过的。
真正的答案来啦~~我在 MSDN 里找得好苦哦 QAQ
真正的原因不在于 Python 怎么样了,而在于 Windows 怎么样了。经查源码(Python 2.7.6)Objects/fileobject.c:2837
,Python 是使用 fopen/fread/fwrite
这系列函数来读写文件的。MSDN 说:
When the "r+", "w+", or "a+" access type is specified, both reading and writing are allowed (the file is said to be open for "update"). However, when you switch from reading to writing, the input operation must encounter an EOF marker. If there is no EOF, you must use an intervening call to a file positioning function. The file positioning functions are fsetpos, fseek, and rewind. When you switch from writing to reading, you must use an intervening call to either fflush or to a file positioning function.
所以会有 @沙渺 发现添加 flush()
调用后正确的结果。
Linux 下没有重现。man 3 fopen
说:
Reads and writes may be intermixed on read/write streams in any order. Note that ANSI C requires that a file positioning function intervene between output and input, unless an input operation encounters end-of-file. (If this condition is not met, then a read is allowed to return the result of writes other than the most recent.) Therefore it is good practice (and indeed sometimes necessary under Linux) to put an fseek(3) or fgetpos(3) operation between write and read operations on such a stream. This operation may be an apparent no-op (as in fseek(..., 0L, SEEK_CUR) called for its synchronizing side effect.
所以 Windows 的这种行为是符合 ANSI C 标准的,但是 Linux 并不(总是)需要这样做。(并且,跨平台的方案是使用文件定位函数而不是 fflush()
。)
请特别阅读 @依云 的答案。分析上游代码和文档,这才是真正坚不可摧的力量。
同仁们,误入歧途了啊!!!别在ASCII的文件上这么深入的追究编码问题啊!!!
我试了一下,果然爽翻:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
f = open(r"C:\Users\776\test.txt","w+")
# 注:w+ truncates the file - 因此文件无论存在与否,结果一致
f.write('hello')
print(f.read())
f.close()
# 结果不贴了,同样不忍直视,传送门:http://paste.openstack.org/show/61806/
我没有深入的究其原因,不过大概能猜到理由是什么:文件的指针位置。
open()
以w+模式开启了一个读写模式的文件,由于是w,所以文件被废弃清空(truncate),此时的文件内容为[EOF]
,开启时的指针为0。此时如果做read()
,则Python发现指针位置就是EOF,读取到空字符串。
在写入hello之后,指针的位置是5,文件在内存中是hello[EOF]
。
但看起来read()的时候,Python仍然去试图在磁盘的文件上,将指针从文件头向后跳5,再去读取到EOF为止。
也就是说,你实际上是跳过了该文件真正的EOF,为硬盘底层的数据做了一个dump,一直dump到了一个从前文件的[EOF]
为止。所以最后得到了一些根本不期待的随机乱字符(这里根本不是编码问题造成的乱码!)。
(看起来似乎还暴露了一些以前文件的内容呢,C:\Anaconda\
神马的 :D)
解决这个问题,你需要在读文件之前,用file对象的flush()
方法,将已修改的文件内容可靠写盘:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
f = open(r"C:\Users\776\test.txt","w+")
# 注:w+ truncates the file - 因此文件无论存在与否,结果一致
f.write('hello') # 此时指针=5,内存内容=`hello[EOF]`
f.flush() # 此时硬盘内容=`hello[EOF]`
print(f.read()) # 此时指针=5,正好在[EOF]上,正确输出''
f.seek(0) # 指针归0
print(f.read()) # 正确从头读出全部内容'hello'
f.close()
这并不是完美的解决方法,因为我总觉得flush()
应该是真正决定写盘之前才做的,而不是read()
一次就flush()
一次。read()
似乎还是优先读取内存缓冲区更有道理。
但总之至少猜测到了一个原因,并一定程度解决问题,更好的方法有待补充吧?