在Windows上使用Python文件时,如何同时使用read()和write()函数?

6
似乎在Windows上,对于使用r+(或r+b)权限打开的文件,在read()之后立即进行write()操作不会更新文件。假设当前目录中有一个名为testfile.txt的文件,其内容如下:
This is a test file.

我执行以下代码:
with open("testfile.txt", "r+b") as fd:
    print fd.read(4)
    fd.write("----")

我希望代码能够打印出 This 并将文件内容更新为以下内容:
This----a test file.

这在Linux上运行良好。但是,当我在Windows上运行它时,消息被正确显示,但文件没有被改变 - 就像write()被忽略了一样。如果我在文件句柄上调用tell(),它会显示位置已经更新(在write()之前是4,之后是8),但文件没有变化。
然而,如果我在write()行之前放置一个显式的fd.seek(4),那么一切都按照我的预期工作。
有人知道Windows下这种行为的原因吗?
供参考,我正在使用Python 2.7.3,在带有NTFS分区的Windows 7上运行。
编辑
响应评论,我尝试了r+brb+ - 官方Python文档似乎暗示前者是规范的。

我在不同的地方调用了fd.flush(),并且像这样在read()write()之间放置一个:

with open("testfile.txt", "r+b") as fd:
    print fd.read(4)
    fd.flush()
    fd.write("----")

...产生了以下有趣的错误:

IOError: [Errno 0] Error

编辑2

间接地,添加flush()有所帮助,因为它引导我找到了this post,描述了类似的问题。如果其中一位评论者是正确的,那么这是底层Windows C库中的一个错误。


+1:这是一个很棒的问题,值得点赞。除非你具有C文件IO的知识,否则问题并不十分明显。 - Abhijit
很抱歉在这里打扰这个问题。我问了一个类似的问题,但我仍然不清楚是否有对所有这些的答案。这是否意味着在Windows平台上写入同一文件并不像文档所说的那样有效,并且需要将“fileopen”对象声明多次以执行读/写操作? - Cryssie
不,你可以随意在同一个文件上组合使用 read()write()。但这仅意味着每次从调用 read() 切换到调用 write() 时,您都应该插入一个 fd.seek(0, os.SEEK_CUR) 的调用,如Abhijit在他的答案中提到的那样。这的效果是说“将指针移动到它已经在的地方”,但这样做可以使其工作(我不会说“修复问题”,因为这显然是一个主观的问题)。如果您这样做,您可以自由混合使用 read()write(),而不需要再次打开文件。此行为仅适用于Windows。 - Cartroo
4个回答

6

Python的文件操作应遵循libc约定,因为内部实现使用C文件IO函数。

引用自fopen man pagecplusplus中的fopen页面

对于以附加方式打开的文件(包含“+”号的文件),允许输入和输出操作,流应在写入操作后跟读取操作之间刷新(fflush)或重新定位(fseek、fsetpos、rewind),或者已经执行读取操作但没有到达文件末尾后跟写入操作。

因此,如果需要在写入后读取文件,则需要fflush缓冲区,并且在读取操作之后进行的写入操作应先于fseek,如fd.seek(0, os.SEEK_CUR)

因此,请将您的代码片段更改为

with open("test1.txt", "r+b") as fd:
    print fd.read(4)
    fd.seek(0, os.SEEK_CUR)
    fd.write("----")

这种行为与类似的C程序的行为一致。
#include <cstdio>
int main()
{   
    char  buffer[5] = {0};
    FILE *fp = fopen("D:\\Temp\\test1.txt","rb+");
    fread(buffer, sizeof(char), 4, fp);
    printf("%s\n", buffer);
    /*without fseek, file would not be updated*/
    fseek(fp, 0, SEEK_CUR); 
    fwrite("----",sizeof(char), 4, fp);
    fclose(fp);
    return 0;
}

没错,这似乎就是它的长处和短处。我认为Windows的实现相当糟糕,需要这样做,但我肯定不会对Windows API的第一个或最后一个抱怨。 (^ _ ^) - Cartroo
这个答案对Python 3仍然适用吗? - user3064538
@Boris - 显然,我刚试了一下,你需要在read之后进行seek才能在特定位置写入。 - wwii

2
看起来这是由于底层Windows库的行为(个人认为存在错误)而不是Python出了问题。在读写之间添加flush()调用(显然是良好实践)后,我遇到了一个零errno的IOError,这与这篇博客文章中讨论的问题相同。
从那篇文章中,我发现了这个Python问题,其中提到了这个问题,并说seek()调用实际上是最好的解决方法,每次从读取切换到写入时都要使用flush()
考虑到所有这些,似乎编写上述代码的最佳方式是:
with open("testfile.txt", "r+b") as fd:
    print fd.read(4)
    fd.flush()
    fd.seek(4)
    fd.write("----")

可能对于任何试图编写可移植代码的人来说,这都是需要牢记的事情。

这不是一个错误。请查看我的答案。 - Abhijit
我个人认为这是一个观点问题 - “已知”的行为并不等同于“正确”的行为。坦率地说,我认为需要使用seek()作为解决方法,而不是预期的行为。我可以理解混合使用stdio和原始文件IO可能需要刷新,但是通过相同的API应始终保持一致 - 如果可以在API中轻松处理,则要求程序员跳过障碍是不完美的行为 - 或者更简洁地说,是一个错误。 - Cartroo
但是通过相同的API进行操作应始终保持一致...并非总是如此,编译器往往会添加超出标准的功能。即使我们认为这是一个问题,它也与Python无关。使用“msvcrt”的类似C实现方式也会表现出相同的行为。 - Abhijit
我完全同意这与Python无关 - 我在我的评论中试图澄清这一点 "...是Windows库中的一个错误,而不是Python". 不过,我认为这是Windows中的一个缺陷 - 无论seek()做什么都可以很容易地自动完成。我并不是说我期望微软修复任何东西,但我认为这至少是一个有争议的错误。但是再次强调,我肯定没有意味着以任何方式是Python的缺陷,它绝对是Windows库,而不是Python。我已经澄清了我的答案,希望能够让这一点清楚。 - Cartroo
我认为可以合理地期望不同编译器之间会存在一些差异,特别是在不同的平台上。因此,在Windows上行为不同并不一定是我所说的一个错误。我所说的错误是,即使write没有改变字节,tell仍然报告了一个新位置!对我来说,如果write将要失败,它应该完全失败。 - John Y
当你说不同的“编译器”时,你是指不同的平台吗?如果我理解正确的话,这是基础stdio库的行为,不应该依赖于你的编译器。无论如何,我同意 - 如果tell()没有显示指针已经移动,那就不会那么糟糕。或者如果我收到了错误,那也没关系。但在我的观点中,默默地做你不期望的事情从来都不是好的实现方式。当然,对于这些事情我们必须要实用主义一些,但指出可能没有帮助的行为对其他人来说并没有错。 - Cartroo

1

你尝试过清除缓存吗?

fd.flush()

这是与操作系统相关的,因为write使用文件系统缓存机制。


1
好的想法——但是它却以一种有趣的方式失败了。如果我在write()之后添加fd.flush(),则没有变化。然而,如果我在read()write()之间添加fd.flush(),那么我会得到IOError: [Errno 0] Error。这似乎是在说“错误:成功”,这有点有趣。 - Cartroo

-1

实现中是否可能误解了“r+b”?据我所知,“rb+”是用于以二进制形式读写。


1
我考虑过这个,但是Python官方文档明确提到了r+b,所以我认为那是标准的。我确实都尝试过,这点我应该提到的。 - Cartroo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接