Python3 打开缓冲参数看起来很奇怪

Python3 open buffering argument looks strange

来自doc

buffering is an optional integer used to set the buffering policy. Pass 0 to switch buffering off (only allowed in binary mode), 1 to select line buffering (only usable in text mode), and an integer > 1 to indicate the size in bytes of a fixed-size chunk buffer. When no buffering argument is given, the default buffering policy works as follows:

Binary files are buffered in fixed-size chunks; the size of the buffer is chosen using a heuristic trying to determine the underlying device’s “block size” and falling back on io.DEFAULT_BUFFER_SIZE. On many systems, the buffer will typically be 4096 or 8192 bytes long. “Interactive” text files (files for which isatty() returns True) use line buffering. Other text files use the policy described above for binary files.

我用文本模式打开一个名为test.log的文件,并将缓冲设置为16。所以我认为块大小是16,当我向文件写入32字节的字符串时。它会调用 write(syscall) 两次。但实际上,它只调用一次。(在 Python 3.7.2 GCC 8.2.1 20181127 Linux 上测试)

import os


try:
    os.unlink('test.log')
except Exception:
    pass


with open('test.log', 'a', buffering=16) as f:
    for _ in range(10):
        f.write('a' * 32)

使用strace -e write python3 test.py 跟踪系统调用,并获得关注

write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 320) = 320

buffering 是什么意思?

此答案适用于 CPython 3.7 Python 的其他实现可能不同。

文本模式下的 open() 函数 returns _io.TextIOWrapper(). The _io.TextIOWrapper() has internal 'buffer' called pending_bytes with size of 8192 bytes (it is hard coded) 并且对于文本模式 w_io.BufferedRandom() 它也有 _io.BufferedWriter() 句柄] 用于文本模式 a_io.BufferedWriter()/_io.BufferedRandom()的大小由open()函数中的参数buffering指定。

当您调用 _io.TextIOWrapper().write("some text") 时,它会将文本添加到内部 pending_bytes 缓冲区中。在一些写入之后,您将填充 pending_bytes 缓冲区,然后将其写入 _io.BufferedWriter() 内的缓冲区。当您还填满 _io.BufferedWriter() 内的缓冲区时,它将被写入目标文件。

当您以二进制模式打开文件时,您将直接获得 _io.BufferedWriter()/_io.BufferedRandom() 对象,该对象使用 buffering 参数中的缓冲区大小初始化。

让我们看一些例子。我将从使用二进制模式的更简单的开始。

# Case 1
with open('test.log', 'wb', buffering=16) as f:
    for _ in range(5):
        f.write(b'a'*15)

跟踪输出:

write(3, "aaaaaaaaaaaaaaa", 15)         = 15
write(3, "aaaaaaaaaaaaaaa", 15)         = 15
write(3, "aaaaaaaaaaaaaaa", 15)         = 15
write(3, "aaaaaaaaaaaaaaa", 15)         = 15
write(3, "aaaaaaaaaaaaaaa", 15)         = 15

在第一次迭代中,它用 15 个字节填充缓冲区。在第二次迭代中,它发现再添加 15 个字节会使缓冲区溢出,因此它首先刷新缓冲区(调用系统 write),然后保存这 15 个新字节。在下一次迭代中,同样的事情再次发生。在缓冲区中的最后一次迭代之后是 15 B,它们在文件关闭时写入(离开 with 上下文)。

第二种情况,我会尝试向缓冲区写入比缓冲区大小更多的数据:

# Case 2
with open('test.log', 'wb', buffering=16) as f:
    for _ in range(5):
        f.write(b'a'*17) 

跟踪输出:

write(3, "aaaaaaaaaaaaaaaaa", 17)       = 17
write(3, "aaaaaaaaaaaaaaaaa", 17)       = 17
write(3, "aaaaaaaaaaaaaaaaa", 17)       = 17
write(3, "aaaaaaaaaaaaaaaaa", 17)       = 17
write(3, "aaaaaaaaaaaaaaaaa", 17)       = 17

这里发生的是,在第一次迭代中,它会尝试写入缓冲区 17 B,但它放不下,因此直接写入文件,缓冲区保持为空。这适用于每次迭代。

现在让我们看看文本模式。

# Case 3
with open('test.log', 'w', buffering=16) as f:
    for _ in range(5):
        f.write('a'*8192)

跟踪输出:

write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 8192) = 8192

首先回想一下 pending_bytes 的大小为 8192 B。在第一次迭代中,它将 8192 字节(来自代码:'a'*8192)写入 pending_bytes 缓冲区。在第二次迭代中,它向 pending_buffer 添加了另一个 8192 字节并发现它大于 8192(pending_bytes 缓冲区的大小)并将其写入底层 _io.BufferedWriter()_io.BufferedWriter() 中的缓冲区大小为 16 B(buffering 参数),因此它将立即写入文件(与情况 2 相同)。现在 pending_buffer 是空的,在第三次迭代中它再次填充了 8192 B。在第四次迭代中它添加了另一个 8192 B pending_bytes 缓冲区溢出并再次像第二次迭代一样直接写入文件。在最后一次迭代中,它将 8192 B 添加到 pending_bytes 缓冲区中,该缓冲区在文件关闭时被刷新。

最后一个示例包含大于 8192 B 的缓冲。另外为了更好的解释,我又添加了 2 次迭代。

# Case 4
with open('test.log', 'w', buffering=30000) as f:
    for _ in range(7):
        f.write('a'*8192)

跟踪输出:

write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 24576) = 24576

迭代次数:

  1. 将 8192 B 添加到 pending_bytes
  2. 将 8192 B 添加到 pending_bytes 但它超过了最大大小,因此它被写入基础 _io.BufferedWritter() 并留在那里(pending_bytes 现在是空的)。
  3. 将 8192 B 添加到 pending_bytes
  4. 将 8192 B 添加到 pending_bytes,但它超过了最大大小,因此它尝试写入底层 _io.BufferedWritter()。但它会超过底层缓冲区的最大容量,因为 16384 + 16384 > 30000(第 16384 B 仍然来自迭代 2)所以它首先将旧的 16384 B 写入文件,然后将那些新的 16384 B(来自 pending_bytes) 到缓冲区。 (现在 pending_bytes 缓冲区再次为空)
  5. 同3
  6. 同4
  7. 当前 pending_buffer 为空,_io.BufferedWritter() 包含 16384 B。在本次迭代中,它用 8192 B 填充 pending_buffer。仅此而已。

当程序离开 with 部分时,它会关闭文件。关闭过程如下:

  1. pending_buffer中的8192 B写入_io.BufferedWriter()(可能是8192 + 16384 < 30000
  2. 将 (8192 + 16384=) 24576 B 写入文件。
  3. 关闭文件描述符。

顺便说一句,目前我不知道为什么 pending_buffer 可以用于缓冲来自 _io.BufferedWritter() 的底层缓冲区。我最好的猜测是它在那里,因为它提高了文件在文本模式下的性能。