Python 3中的tell()在追加+读取模式下与文件指针不同步

4
与Python 2(2.7.15)不同,当二进制文件以追加和读取方式打开时,我发现在Python 3(3.6.5)中出现了奇怪的f.tell()行为。如果当前寻找位置在文件末尾时写入n个字节,则以下几件事情似乎会按预期发生:

  1. 文件指针将移动到文件末尾。
  2. n个字节被写入。
  3. n被添加到文件指针中。

然而,似乎f.tell()没有注意到第1步,因此f.tell()返回的值相对于实际文件指针偏移了一个恒定的负值。我在Windows和Linux上都看到了相同的情况。

这是一些演示问题的Python 3代码:

import io

# Create file with some content
f = open('myfile', 'wb')
f.write(b'abc')
print(f.tell())                 # 3
f.close()

# Now reopen file in append+read mode and check that appending works
f = open('myfile', 'a+b')
print(f.tell())                 # 3
f.write(b'def')                 # (Append)
print(f.tell())                 # 6

# Now seek to start of file and append again -> tell() gets out of sync!
print(f.seek(0))                # 0
print(f.tell())                 # 0
f.write(b'ghi')                 # (Append)
print(f.tell())                 # 3!!! (expected 9)
f.write(b'jkl')                 # (Append)
print(f.tell())                 # 6!!! (expected 12)

# Try calling seek without moving file pointer -> tell() starts working again
print(f.seek(0, io.SEEK_CUR))   # 12 (correct)
print(f.tell())                 # 12 (correct)

# Read whole file to verify its contents
print(f.seek(0))                # 0
print(f.read())                 # b'abcdefghijkl' (correct)
f.close()

Python 3文档警告不要在文本文件上使用seek()/tell()(请参见io.TextIOBase),并且在某些平台上有一个关于追加模式的警告(请参见open()):
[...] 'a' for appending (which on some Unix systems, means that all writes append to the end of the file regardless of the current seek position).
但我正在使用二进制文件,写入似乎确实是追加到文件末尾而不考虑寻址位置,所以我的问题不同。
我的问题是:这种行为是否有记录(直接或间接),或者至少记录了该行为未指定?
编辑:
文本文件似乎没有这个问题(无论在Python 2还是3中),因此只有二进制文件不能按预期工作。
Python 3文档(io.TextIOBase)指出,对于文本文件,tell()返回一个“不透明”的值(即未指定该值如何表示位置),由于未提及此是否也适用于二进制文件,因此可能会认为我的问题与此不透明性有关。然而,这是不可能的,因为即使是不透明的值,在传递给seek()时,也必须将文件指针返回到调用tell()时的位置,在上面的示例中,当tell()在同一文件位置(文件结尾)返回6和12时,只有seek(12)才会将文件指针实际移动到该位置。因此,值6不能用文件指针不透明性来解释。

当tell()返回3而你期望它返回9时,read()会做什么? - interfect
@interfect,如果在f.tell()返回3(期望值为9)后执行f.read(),则会得到预期的空字节对象b'',表明文件指针实际上已经到达文件末尾而不是位置3。 f.read()的一个副作用是f.tell()将重新开始工作,并为相同的位置报告9。 - Ovaflo
2个回答

0
  • 当您调用f.seek(t, offset)时,您将文件对象的位置更改为t + offset
  • written = f.write(data)会将位置向前移动written个字节
  • f.tell()返回文件中的当前位置

因此,这里没有问题:

f.seek(0) # position = 0
f.write(b'123') # position += len(b'123') => position = 3
f.tell() # return position, which is equal to 3

数据直接写在当前位置之后,因此在这种情况下您不会添加任何内容,而是覆盖现有数据。或者至少应该是这样,但是根据文档中的引用,这种行为可能会有所不同。


1
不,我是以追加模式打开文件的,所以我没有覆盖任何内容。请再次查看代码示例。 - Ovaflo
@Ovaflo,是的,但位置会根据对seek的调用而改变。 - ForceBru
是的,但是追加操作发生在文件末尾,因此tell()应该反映出这一点。 - Ovaflo

0

就像我在issue36411中回复的那样

我不确定这是否是一个错误。当您将二进制数据写入文件时(默认使用BufferedIOBase),它实际上会将数据写入缓冲区。这就是为什么tell()会失去同步的原因。您可以按照下面的方法进行操作。例如,在写入后调用flush()以获得正确的答案。

当向此对象写入数据时,通常会将数据放入内部缓冲区。在以下情况下,缓冲区将被写入底层RawIOBase对象:

  1. 当缓冲区对于所有待处理数据来说太小时;
  2. 当调用flush()时;
  3. 当请求seek()(对于BufferedRandom对象)时;
  4. 当关闭或销毁BufferedWriter对象时。
  1. https://docs.python.org/3/library/io.html#io.BufferedWriter

那么为什么tell不会触发缓冲区刷新呢?或者至少返回正确的值,即tell加上写缓冲区的长度。 - Dan D.
缓冲不会阻止tell()在其他文件模式下正常工作,那么为什么在这种模式下会呢?即使在Python 2中,这种模式也可以正常工作。 - Ovaflo

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