来自doc
buffering是一个可选的整数,用于设置缓冲策略。传递0以切换缓冲关闭(仅允许在二进制模式下),1选择行缓冲(仅在文本模式下可用),并且整数> 1以指示固定大小的块缓冲区的大小(以字节为单位)。如果没有给出缓冲参数,则默认缓冲策略的工作方式如下:
二进制文件以固定大小的块缓冲;使用启发式方法选择缓冲区的大小,尝试确定底层设备的“块大小”并回退到io.DEFAULT_BUFFER_SIZE。在许多系统上,缓冲区通常为4096或8192字节长。 “交互式”文本文件(isatty()返回True的文件)使用行缓冲。其他文本文件使用上述策略用于二进制文件。
我用文本模式打开一个名为test.log
的文件,并将缓冲设置为16.所以我认为块大小是16,当我向文件写入32字节字符串时。它会两次调用write
(系统调用)。但实际上,它只调用一次。(在Linux上测试Python 3.7.2 GCC 8.2.1 20181127)
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()
函数返回_io.TextIOWrapper()
。 _io.TextIOWrapper()
有内部'缓冲区',称为pending_bytes
,大小为8192字节(它是hard coded),它还有_io.BufferedWriter()
处理文本模式w
或_io.BufferedRandom()
文本模式a
。 _io.BufferedWriter()
/ _io.BufferedRandom()
的大小由buffering
函数中的open()
参数指定。
当您调用_io.TextIOWrapper().write("some text")
时,它会将文本添加到内部pending_bytes
缓冲区中。在一些写入之后,您将填充pending_bytes
缓冲区,然后它将被写入_io.BufferedWriter()
内的缓冲区。当你填满_io.BufferedWriter()
中的缓冲区时,它将被写入目标文件。
当您以二进制模式打开文件时,您将直接获得使用_io.BufferedWriter()
parametr的缓冲区大小初始化的_io.BufferedRandom()
/ buffering
对象。
我们来看一些例子。我将从更简单的使用二进制模式开始。
# Case 1
with open('test.log', 'wb', buffering=16) as f:
for _ in range(5):
f.write(b'a'*15)
strace输出:
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)
strace输出:
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)
strace输出:
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 8192) = 8192
首先回想起pending_bytes
的大小为8192 B.在第一次迭代中,它将8192个字节(来自代码:'a'*8192
)写入pending_bytes
缓冲区。在第二次迭代中,它将另外的8192个字节添加到pending_buffer
,并发现它超过8192(pending_bytes
缓冲区的大小)并将其写入底层的_io.BufferedWriter()
。 _io.BufferedWriter()
中的缓冲区大小为16 B(buffering
参数),因此它会立即写入文件(与案例2相同)。现在pending_buffer
是空的,并且在第三次迭代中它再次填充8192B。在第四次迭代中,它再添加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)
strace输出:
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 16384) = 16384
write(3, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 24576) = 24576
迭代:
pending_bytes
。pending_bytes
但它超过最大大小,因此它被写入底层_io.BufferedWritter()
并且它保持在那里(pending_bytes
现在是空的)。pending_bytes
。pending_bytes
中,但它超过了最大值,因此它试图写入底层的_io.BufferedWritter()
。但它会超过底层缓冲区的最大容量导致16384 + 16384 > 30000
(第一次16384 B仍然存在于迭代2中)所以它首先将旧的16384 B写入文件,然后将这些新的16384 B(来自pending_bytes
)放入缓冲区。 (现在再次pending_bytes
缓冲区为空)pending_buffer
是空的,_io.BufferedWritter()
包含16384 B.在这个迭代中,它填充pending_buffer
与8192 B.这就是它。当程序离开with
部分时,它关闭文件。关闭过程如下:
pending_buffer
写入8192 B到_io.BufferedWriter()
(有可能导致8192 + 16384 < 30000
)8192 + 16384
=)24576 B写入文件。顺便说一下,目前我不知道为什么有pending_buffer
它可以用来缓冲来自_io.BufferedWritter()
的底层缓冲区。我最好的猜测就是它,因为它可以提高文本模式下工作的性能。