在'r +'中,为什么*在*读取一行之后写文本文件使它写在末尾而不是`f.tell()`位置?

问题描述 投票:3回答:2

具有这样的文本文件:

line one
line two
line three

并运行以下代码:

with open('file', 'r+') as f:
    print(f.tell())
    print(f.readline().strip())
    print(f.tell())
    # f.seek(f.tell())
    f.write('Hello')
    print(f.tell())

结果是在文件的最后写上“ Hello”一词:

line one
line two
line threeHello

我以为书写部分将从最近读取的字符位置开始(在line one之后),但不是,除非我取消对f.seek(f.tell())的注释。我可能缺少一些基本知识,但我在Python文档中找不到任何能深入解释其工作原理的内容。这是怎么回事,是什么使它在那写了字?如果我不先阅读而是开始写作,为什么不发生这种情况?

f.tell()的打印值如下:

0
9
39
python file-writing
2个回答
3
投票

[这似乎是io.TextIOWrapper(在文本模式下由open返回的类)与io.BufferedRandom(在+模式下包装的类)进行交互的错误。

如果将测试用例更改为以二进制模式运行:

with open('file', 'rb+') as f:
    print(f.tell())
    print(f.readline().strip())
    print(f.tell())
    # f.seek(f.tell())
    f.write(b'Hello')
    print(f.tell())

无论是否包含多余的f.seek(f.tell()),其行为都是相同的。

该问题似乎是由于所涉及的多层缓冲引起的。您得到的是包装了io.TextIOWrapperio.BufferedRandom(反过来又包装了io.FileIO)。 TextIOWrapperio.BufferedRandom读取块以分摊从字节到文本的解码成本,因此,当您调用readline时,它实际上是在消耗并解码整个文件(它很小,只适合一个块),因此BufferedRandom位于文件的末尾(即使从逻辑上讲,它应该只位于文件的中间,并且TextIOWrapper.tell报告与该逻辑位置相对应的位置)。

[当您转向write时,TextIOWrapper对数据进行编码并将其传递给BufferedRandom,后者仍然认为自己位于文件的末尾;由于TextIOWrapper不能更正此问题,因此数据将附加到最后。看似无操作的f.seek(f.tell())TextIOWrapper与下标BufferedRandom重新同步,以获得预期的行为。确实没有必要(我建议filing a bug确保write移至逻辑tell的位置,因为虽然Python 3 f.tell() gets out of sync with file pointer in binary append+read mode表面上相似,但我找不到现有的错误),但至少解决方法相对简单。


1
投票

问题与缓冲的IO有关。

open()函数似乎打开了一个缓冲文件句柄。

因此,实际上,每当读取文件中的内容时,至少会读取整个缓冲区,该缓冲区似乎在我的计算机上大约为8k(8192)字节。这是为了优化性能。

因此readline将读取一个块,并返回第一行,并将其余的保留在缓冲区中,以备将来读取。

f.tell()给出相对于readline()已返回的字节的位置。

这可以通过f.seek(f.tell())强制执行写指针到您想要的地方。如果没有显式的seek语句,您将在缓冲区之后编写。

使用以下脚本来说明和查看输出:

您将看到,我尝试使用buffering参数。根据文档1的意思是行缓冲,但是我看不到任何行为上的变化。

with open("file", "w") as f:
    f.write(("*" * 79 +"\n") * 1000)

with open('file', 'r+', buffering=1) as f:
    print(f.tell())
    print(f.readline().strip())
    print(f.tell())
    # f.seek(f.tell())
    f.write('Hello')
    print(f.tell())

print("----------- file contents")
with open("file", "r") as f:
    pass
    print(f.read())
print("----------- END")

因此,如果您在readline()之后写入,那么它将在读取的缓冲区之后写入新数据。

另一方面,f.tell()返回您的位置,它告诉您已经返回了多少字节。

输出将是:

0
*******************************************************************************
80
8197
8202
----------- file contents
*******************************************************************************
*******************************************************************************
...
*******************************************************************************
********************************HelloHello*************************************
*******************************************************************************
*******************************************************************************
*******************************************************************************
*******************************************************************************
...
© www.soinside.com 2019 - 2024. All rights reserved.