0

0

深入理解Python文件I/O中read()与write()的交互行为

碧海醫心

碧海醫心

发布时间:2025-10-22 11:08:17

|

819人浏览过

|

来源于php中文网

原创

深入理解Python文件I/O中read()与write()的交互行为

python中,使用r+模式进行文件读写时,read()和write()操作的交替使用可能导致文件指针行为出乎意料,尤其是在内部缓冲机制的作用下。read()操作会预先读取数据块到内存缓冲区,而随后的write()操作可能不会紧随read()的逻辑位置,而是作用于实际文件指针,该指针可能已因缓冲而大幅提前。理解并正确使用f.flush()和f.seek()是解决此问题的关键。

1. Python文件I/O基础回顾

Python提供了多种文件操作模式,其中:

  • 'w' (写入模式): 打开文件用于写入。如果文件已存在,其内容将被截断。
  • 'r' (读取模式): 打开文件用于读取。
  • 'r+' (读写模式): 打开文件用于读写。文件指针初始位于文件开头。

在文件操作中,f.tell()方法用于获取当前文件指针的位置(以字节为单位),而f.seek(offset, whence)方法则用于移动文件指针。whence参数可选,默认为0(文件开头),1(当前位置),2(文件末尾)。

2. read()与write()在r+模式下的异常行为

当在r+模式下交替执行read()和write()操作时,可能会观察到出乎意料的文件内容修改。考虑以下示例:

with open('test.txt', 'w') as f:
    f.write('HelloEmpty') # 创建一个包含 'HelloEmpty' 的文件

with open('test.txt', 'r+') as f:
    print(f.read(5))     # 读取前5个字符
    print(f.write('World')) # 写入 'World'
    f.flush()            # 刷新缓冲区
    f.seek(0)            # 将文件指针移回开头
    print(f.read(10))    # 再次读取前10个字符

你可能期望输出如下:

立即学习Python免费学习笔记(深入)”;

Hello
5
HelloWorld

但实际输出却是:

Hello
5
HelloEmpty

并且文件test.txt的内容变成了HelloEmptyWorld。这表明write('World')操作并没有发生在read(5)之后,即文件指针的逻辑位置。

为了进一步揭示问题,考虑一个更大的文件:

with open('test.txt', 'w') as f:
    for _ in range(10000):
        f.write('HelloEmpty') # 创建一个大文件

with open('test.txt', 'r+') as f:
    print(f.read(5))
    print(f.write('World'))

执行这段代码后,检查test.txt文件,你会发现'World'这个词被写入到了文件中的第8193个字符位置,而不是预期的第6个字符位置。

3. 内部缓冲机制的原理

这种看似“异常”的行为源于Python文件I/O的内部缓冲机制。为了提高性能,Python在读取文本文件时,并不会每次都直接从磁盘读取少量数据。相反,它会预先读取一个较大的数据块(通常是8192字节)到内部缓冲区。

文心快码
文心快码

文心快码(Comate)是百度推出的一款AI辅助编程工具

下载
  • read()操作:当调用f.read(n)时,Python会尝试从这个内部缓冲区中返回n个字符。如果缓冲区数据不足,它会从磁盘读取下一个8192字节的数据块来填充缓冲区。文件对象的内部逻辑指针会跟踪在缓冲区中的当前位置。
  • write()操作:然而,当在r+模式下执行write()操作时,尤其是在read()之后,write()可能不会使用read()操作所维护的逻辑指针。相反,它可能会使用底层的操作系统文件指针,而这个指针可能已经因为read()操作预读整个8192字节缓冲区而前进到了缓冲区的末尾(或文件末尾,以先到者为准)。

这意味着,尽管你的read(5)只消费了缓冲区的前5个字符,但底层的实际文件指针可能已经移动了8192字节。随后的write()操作将从这个“实际”文件指针位置开始写入。

字符与字节的差异:如果文件使用多字节编码(如UTF-16),这个缓冲区的8192字节可能不对应8192个字符。例如,使用utf16编码时,一个字符可能占用2个字节。在这种情况下,8192字节的缓冲区将包含4096个字符,write()操作会在第4097个字符位置(即8192字节之后)写入。

4. 解决策略:flush()与seek()的协同作用

为了确保read()和write()操作在r+模式下能够按照预期修改文件内容,关键在于同步Python的内部缓冲区状态与底层的实际文件指针。这可以通过f.flush()和f.seek()方法实现。

  • f.flush():强制将所有待写入的数据从Python的内部缓冲区写入到操作系统缓冲区,甚至直接写入磁盘(取决于操作系统)。这确保了在执行seek()之前,所有挂起的写入操作都已完成。
  • f.seek(0):将文件指针精确地移动到文件开头,或者其他任何指定的位置。

考虑以下对比示例,它清晰地展示了read()后不刷新和重定位文件指针可能带来的问题:

# 示例 1: read() 后没有 flush() 和 seek()
with open('test1.txt', 'w') as f:
    f.write('x' * 100000) # 写入10万个 'x'

with open('test1.txt', 'r+') as f:
    s1 = f.read(5)    # 1. 读取前5个字符 ('xxxxx')
    f.seek(0)         # 2. 将文件指针移回开头
    f.write('y' * 5)  # 3. 写入5个 'y'
    f.read(5)         # 4. 再次读取5个字符 (此操作会再次触发缓冲区预读)
    f.flush()         # 5. 刷新缓冲区
    f.seek(0)         # 6. 将文件指针移回开头
    s2 = f.read(5)    # 7. 读取前5个字符
print(f"test1.txt: s1='{s1}', s2='{s2}'")

# 示例 2: read() 后有 flush() 和 seek() (或避免在write前再次read)
with open('test2.txt', 'w') as f:
    f.write('x' * 100000)

with open('test2.txt', 'r+') as f:
    s1 = f.read(5)    # 1. 读取前5个字符 ('xxxxx')
    f.seek(0)         # 2. 将文件指针移回开头
    f.write('y' * 5)  # 3. 写入5个 'y'
    # 注意:这里没有 f.read(5) 再次触发缓冲区预读
    f.flush()         # 4. 刷新缓冲区
    f.seek(0)         # 5. 将文件指针移回开头
    s2 = f.read(5)    # 6. 读取前5个字符
print(f"test2.txt: s1='{s1}', s2='{s2}'")

输出结果:

test1.txt: s1='xxxxx', s2='xxxxx'
test2.txt: s1='xxxxx', s2='yyyyy'

从test1.txt的输出可以看到,即使在写入'y'并flush()、seek(0)之后,再次读取到的仍然是'xxxxx'。这是因为在f.write('y' * 5)之后,f.read(5)操作再次触发了缓冲区的预读,并且由于之前的write()可能还没有完全同步到文件,或者read()再次填充了缓冲区,导致后续的read(5)读取的仍然是旧数据或者被缓冲机制干扰的数据。

而test2.txt的输出则符合预期,'yyyyy'被正确写入并读取。这强调了在read()和write()之间切换时,如果需要精确控制文件指针,应该避免在write()之后紧接着read(),除非你明确知道其行为。更稳妥的做法是:在从读取切换到写入,或者从写入切换到读取时,始终调用f.flush()来清空缓冲区,然后调用f.seek()来重新定位文件指针。

5. 最佳实践与注意事项

  1. 明确模式用途:r+模式虽然灵活,但也容易引入混淆。如果只是读取,用'r';如果只是写入(且不关心覆盖),用'w'。只有当需要原地修改文件内容时,才考虑'r+'。
  2. flush()和seek()的必要性:在r+模式下,当你从读取操作切换到写入操作,或者从写入操作切换到读取操作时,强烈建议:
    • 先调用f.flush()确保所有挂起的写入操作都已提交到操作系统。
    • 再调用f.seek(position)将文件指针移动到你希望进行下一个操作的精确位置。
  3. 理解缓冲机制:认识到Python的文本I/O层有内部缓冲区,这会影响文件指针的实际行为。
  4. 二进制模式考虑:如果需要对文件进行精确的字节级操作,并且不希望受到文本编码和内部缓冲的复杂性影响,可以考虑使用二进制模式(如'rb+')。在二进制模式下,文件I/O通常更直接地映射到操作系统调用,行为可能更可预测。

总结

Python文件I/O的内部缓冲机制在提高性能的同时,也为r+模式下的read()和write()交替操作带来了潜在的困惑。当read()预读大量数据到缓冲区时,随后的write()操作可能不会从read()的逻辑结束位置开始,而是从实际文件指针(可能已因缓冲而大幅提前)开始。通过在读写操作切换时,显式地调用f.flush()来同步缓冲区,并使用f.seek()来精确重定位文件指针,可以有效避免这些意外行为,确保文件操作的准确性和可预测性。

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

765

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

639

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

764

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

619

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1285

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

549

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

579

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

709

2023.08.11

PS使用蒙版相关教程
PS使用蒙版相关教程

本专题整合了ps使用蒙版相关教程,阅读专题下面的文章了解更多详细内容。

23

2026.01.19

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 5.8万人学习

Django 教程
Django 教程

共28课时 | 3.3万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号