文件操作是Python学习路上绕不开的一道坎,也是从"写小程序自娱自乐"跨到"真正处理日常任务"的分水岭。很多零基础的朋友一开始接触文件操作,就是照着教程抄几行open()和read(),能跑通就觉得自己会了,结果一到实际场景——读CSV乱码、文件写进去不见了、路径总是报错——就傻眼了。这篇文章我就从零开始,把Python文件操作这条线完整讲透,从最基础的打开关闭,到读写模式的取舍,再到目录遍历和常见坑的排查,帮你把这块地基打结实。无论你是刚看完语法还没碰过文件的新手,还是写了几百行脚本但总在文件处理上翻车的人,这篇都能给你一些实打实的东西。
1. 先把文件操作的全局认知建立起来
1.1 文件操作到底在解决什么问题
先说个本质的问题:程序跑完就没了,内存里的数据关机就清空,那你怎么把结果留下来?怎么在下一次运行时接着用?怎么把一批数据批量处理?答案就是文件。文件是程序和持久化存储之间的桥梁,文件操作就是从这座桥上搬运数据的能力。
Python的文件操作主要分两个层次。第一层是单个文件层面,也就是open、read、write、close这一套,解决的是"把某个文件的内容读进内存"或者"把内存中的数据写进某个文件"。第二层是目录与批量操作层面,比如遍历文件夹下所有文件、批量改名、按规则筛选文件,这些需要借助os和pathlib模块。很多人只学了第一层就去干活,遇到批量处理就只会复制粘贴几十遍代码,本质上是对第二层不熟悉。
我建议零基础的朋友按这条线来学:先掌握单个文件的读写,然后是with语句和编码处理,接着是文件路径和目录遍历,最后才是异常处理和批量任务。顺序不要反,路径和目录这些概念放到前面讲容易劝退,因为你还不知道为什么要用它。
1.2 我建议的学习路线:从读文件到管文件
这条路线分四步走。第一步是会用:能读文本文件、能写文本文件,知道r、w、a这几个模式的区别。第二步是用稳:学会with语句自动关闭文件,学会处理编码问题,知道什么时候用二进制模式。第三步是用好:掌握按行读取大文件、批量操作目录、用pathlib写出清晰的路径代码。第四步是用活:结合异常处理、临时文件、json和csv这些内置模块,写出健壮的文件处理脚本。
每一步都有对应的实战练习。第一步练习是写一个"记事本":输入内容,存到文件里,下次打开还能读出来。第二步练习是写一个日志记录器:程序运行过程中不断往文件里追加日志。第三步练习是写一个批量文件整理脚本:把某个目录下的所有文件按扩展名分类归档。第四步练习是把一个CSV批量转成JSON,同时处理掉格式不干净的数据行。这几个练习做下来,文件操作这块就算真入门了。
2. 核心细节:打开、关闭与读写模式
2.1 open()函数与模式选择
所有文件操作的起点都是open()函数。它最常用的签名是open(file, mode, encoding),file是文件路径,mode是打开模式,encoding是编码方式。关键是这个mode参数,它决定了你对文件能做什么、怎么做。
常见的模式有这几个:r只读、w写入覆盖、a追加写入、rb二进制读、wb二进制写、r+读写、w+读写覆盖。初学者最容易搞混的是w和a,w会把文件原有内容清空再从头写,a是在文件末尾追加。我见过太多人写日志用w模式,跑一次程序之前的数据全没了,这是个特别痛的教训。
r+和w+这两个读写模式也很容易让人困惑。r+打开文件要求文件必须存在,读写初始位置在开头,但不会清空内容,写入时会覆盖对应位置的内容,读到的还是旧内容和新内容的混合。w+则会直接清空文件,等于先砍掉重来再让你读写。实际开发中r+偶尔用来修改文件中间某一段,w+用得很少,大多数场景用r、w、a三件套就够了。
注意:把'w'、'a'、'r+'组合使用时要格外小心,特别是w和w+,一旦文件名写错指向了不该动的文件,内容就没了。建议实测前先备份,或者在测试目录里试运行。
2.2 with语句的正确打开方式
很多教程会让你这样写:
f = open('data.txt', 'r', encoding='utf-8') content = f.read() f.close()这段代码能跑,但它埋着一个隐患:如果f.read()中间抛了异常,f.close()就不会执行,文件句柄一直占着,Windows下还会导致文件被锁定,等你下次想删这个文件时系统会提示"文件正在被使用"。
正确做法是用with语句:
with open('data.txt', 'r', encoding='utf-8') as f: content = f.read()with语句会在代码块结束或者抛出异常时自动调用close(),这是Python上下文管理器协议在文件对象上的体现。不用你手动关,也不会忘。我自己写脚本已经习惯了所有文件都用with打开,哪怕只是读一个很小的配置,也不图省事。
这里提一个很多人不知道的点:with语句的缩进块就是文件的有效作用域,出了这个缩进块,虽然文件对象f还在,但文件已经关闭了,再读就会报ValueError: I/O operation on closed file。所以如果你要在with外面继续处理文件内容,记得先把数据读到普通变量里,再在外面操作。
2.3 编码问题:最容易踩的坑
编码问题是我见过零基础初学者报错率最高的一类,没有之一。症状通常是这样的:用open('xxx.txt', 'r')读一个文件,结果一运行,抛UnicodeDecodeError: 'utf-8' codec can't decode byte...,或者读出来是一堆乱码。
原因很简单:文件本身不是UTF-8编码的,你用UTF-8的规则去解码,自然对不上。Windows下面很多老软件生成的文件是GBK或者GB2312编码,而Python 3的open()默认编码是UTF-8,于是就不兼容。
解决方案有两个方向。第一个方向是读文件时指定正确的编码:
with open('data.txt', 'r', encoding='gbk') as f: content = f.read()第二个方向是在编码不确定时,先尝试用二进制方式读入,再用errors参数兜底:
with open('data.txt', 'rb') as f: raw = f.read() # 优先尝试utf-8,失败则用gbk try: text = raw.decode('utf-8') except UnicodeDecodeError: text = raw.decode('gbk', errors='replace')还有一个更省心的做法:直接用errors='ignore'或errors='replace':
with open('data.txt', 'r', encoding='utf-8', errors='replace') as f: content = f.read()errors='replace'会把无法解析的字节替换成替代字符(通常是),不会报错,但内容可能有损。errors='ignore'则直接跳过无法解析的部分。这两种方式适合你只想快速拿数据、不在意个别字符的场景,但如果数据是重要资料,我不建议用了就完事,最好还是搞清楚文件真正的编码。
至于写入时的编码,同样要注意。在Windows上如果你直接open(filename, 'w')不指定encoding,Python 3默认用的是平台编码,中文系统通常是GBK。这样写出来的文件,在别的机器上或者上传到服务器时可能是乱码。我的习惯是写入文件时显式加上encoding='utf-8',让文件在所有平台上保持一致,这是最稳妥的做法。
3. 实操过程:从零开始完成文件读写全流程
3.1 文件的读取:read、readline、readlines的取舍
读取文件有三个常用方法:read()一次性读全部、readline()一行一行读、readlines()读取全部行返回列表。很多教程把三个方法都列出来,但不告诉你什么时候该用哪个,导致很多人不管文件多大一律read()。
read()的问题在于它会把整个文件载入内存。文件如果只有几百KB,无所谓;但如果是一个几GB的日志文件,内存直接爆掉。我实测过一个800MB的文本文件,用read()读入内存后进程占用直接到1.6GB以上,因为Python字符串和底层文件字节之间还有转换开销。这种场景必须用逐行读取。
逐行读取最常见的写法是直接for循环遍历文件对象:
with open('big_log.txt', 'r', encoding='utf-8') as f: for line in f: # 处理每一行 process_line(line)这种方式每次只往内存里放一行,内存占用几乎可以忽略。readline()是把for循环手动展开的写法,适合你在循环中需要额外控制、比如读一行跳两行的场景。readlines()则会一次性把所有行塞进一个列表,如果文件有上百万行,列表会非常占内存,一般不推荐大文件使用。
我个人的选择标准很简单:文件小于50MB用read()或readlines(),省事;文件大于50MB用for line in f,保命。中间还有一个折中的办法,就是分块读取,适合读取超大文件时指定每次读取多少字节:
with open('big_log.txt', 'r', encoding='utf-8') as f: while True: chunk = f.read(8192) if not chunk: break process_chunk(chunk)3.2 文件的写入:write与writelines的正确用法
写入文件最核心的方法是write(),它一次写入一个字符串。还有writelines(),它接收一个可迭代对象,把多个字符串依次写入。要注意writelines并不会自动加换行符,它只是把列表里的每个字符串逐个写进去,换行你得自己在字符串里带\n。很多人以为writelines会自动加换行,结果写出来所有内容黏在一起,这是这个函数最常见的误解。
我写文件时的标准姿势是这样的:
lines = [ '第一行\n', '第二行\n', '第三行\n', ] with open('output.txt', 'w', encoding='utf-8') as f: f.writelines(lines)如果你要写入的数据本来就在一个列表里,比如从CSV读出来的行,writelines比循环write()快不少。但如果你是在一段逻辑里逐步凑内容,那就直接用write()往同一个文件对象上多次调用,它会在上次写入的末尾接着写,不会覆盖。
还有一个细节:频繁调用write()会有多次系统调用开销。对于大量小写入,性能会明显下降。我的实测经验是,如果是一次性写入几百行,循环write()和writelines差别不大;但如果是几十万行,writelines或者先把内容拼成大字符串再一次性write()会快好几倍。核心思路是减少系统调用次数。
3.3 文件复制与移动的几种实现
文件复制是日常脚本里特别常见的需求。最简单的实现是手动读写:
with open('source.txt', 'rb') as src: with open('dest.txt', 'wb') as dst: dst.write(src.read())这段代码对小文件没问题,但对大文件来说,src.read()把所有内容都读进内存,浪费且危险。正确做法是分块复制:
with open('source.txt', 'rb') as src: with open('dest.txt', 'wb') as dst: while True: chunk = src.read(64 * 1024) # 每次64KB if not chunk: break dst.write(chunk)不过既然Python给我们提供了shutil模块,这类操作就不要自己造轮子了。shutil.copyfile(src, dst)底层就是帮我们做了分块复制,实现得很稳。更高层的还有shutil.copy(),它会复制文件内容并尝试保留权限,shutil.copy2()则还会尝试保留元数据比如修改时间。移动文件用shutil.move(),它既能跨目录移动,底层会根据目标路径判断是同盘rename还是跨盘复制加删除,省心。
经验之谈:自己在循环里写文件复制代码适合学习原理和面试演示,日常脚本里直接用shutil即可。区别就像你可以在家里自己炒菜,但没必要为了吃顿饭去种地。
4. 进阶操作:目录遍历、缓冲与上下文管理
4.1 os和pathlib模块怎么选
到了目录和路径这块,你一定会碰到os和pathlib两个选择。os模块是老牌模块,提供的函数像os.path.join、os.listdir、os.path.exists等等,用起来没什么问题,但代码会比较啰嗦,路径拼接时万一忘用os.path.join,直接写'/',在Windows上就会出问题。
pathlib是Python 3.4引入的面向对象路径库,把路径封装成了Path对象。比如:
from pathlib import Path p = Path('data') / 'subdir' / 'file.txt' if p.exists(): print(p.read_text(encoding='utf-8'))相比os.path.join('data', 'subdir', 'file.txt'),pathlib直接用/操作符拼接路径,代码直观很多,而且跨平台自动处理斜杠方向。我个人现在的新代码一律用pathlib,只有一些老脚本还在用os。不是说os不好,而是pathlib的体验确实更顺手,特别是处理多个路径、判断后缀、取文件名这些操作,Path对象有现成的方法。
举几个常用的对比:
# os写法 import os parent = '/tmp/data' filename = 'test.txt' full_path = os.path.join(parent, filename) basename = os.path.basename(full_path) exists = os.path.exists(full_path) # pathlib写法 from pathlib import Path full_path = Path('/tmp/data') / 'test.txt' basename = full_path.name exists = full_path.exists()pathlib里比较常用的属性和方法:.name获取文件名、.stem获取去掉后缀的文件名、.suffix获取扩展名、.parent获取父目录、.exists()判断是否存在、.is_file()判断是否为文件、.is_dir()判断是否为目录、.read_text()和.write_text()直接读写文本。这些方法记住之后,很多文件处理代码会变得极其简洁。
4.2 目录遍历的几种方式
批量处理文件时,核心需求是遍历目录。最基础的是os.listdir(),它返回指定目录下所有条目名称的列表,但不会递归子目录,也不会告诉你条目是文件还是目录。要区分文件还是目录,还得自己逐个os.path.isdir()判断,比较麻烦。
pathlib提供了更优雅的遍历方式。glob()支持通配符:
from pathlib import Path # 获取当前目录下所有.txt文件 for p in Path('.').glob('*.txt'): print(p) # 递归遍历所有子目录下的.py文件 for p in Path('.').rglob('*.py'): print(p)rglob是递归版glob,一个方法就搞定"把某个目录下所有指定类型的文件找出来"这件事,非常常用。我用它写过批量整理下载目录的脚本:把所有.jpg归档到图片文件夹,把所有.pdf归档到文档文件夹,循环里用p.suffix判断扩展名,然后shutil.move,几十行代码解决一个重复性的手工活。
如果你需要更精细的控制,比如要跳过某些目录、按修改时间筛选文件,可以用os.walk()。它每次返回当前目录路径、子目录列表、文件列表三个值,你可以自由决定要不要进入某个子目录。配合filter可以玩出很多花样,但代码会相对长一些。大部分场景下pathlib的rglob就能覆盖需求,只有少数复杂规则才需要os.walk()出手。
4.3 文件操作中的缓冲与性能细节
文件操作里有个影响性能但很少被注意的因素:缓冲。Python的open()默认是有缓冲的,写入数据时不会立刻刷到磁盘,而是先存到内存缓冲区,满了或者关闭文件时才真正写盘。这个机制能大幅提升性能,因为系统调用是很贵的。
但缓冲也会带来一个问题:如果你写入后马上断电或程序崩溃,缓冲区里没来得及落盘的数据会丢。如果你要写入的内容至关重要,可以在写入后调用f.flush()强制把缓冲区的数据写到磁盘。或者,如果你完全不需要缓冲,可以给open()的buffering参数传0,但一般没人这么做,因为性能损失太大。
还有一个和缓冲密切相关的细节:当你用w模式写文件时,每次运行都会重新创建文件,之前的内容全部消失。如果你希望"程序崩溃时已经写好的部分还在",可以考虑流式追加或者边写边flush配合错误处理。
我在写日志类脚本时,会开一个写缓冲、每写满一定行数或者每隔几秒主动flush一次,这样既不会频繁刷盘拖慢速度,也不会在崩溃时丢太多日志。具体实现是自建一个计数器,或者在循环里判断当前行数与上次flush时行数之差。
5. 常见问题与排查技巧实录
5.1 编码问题全家桶:UnicodeDecodeError与其他症状
把编码问题单独拿出来细说,因为它值得。我整理了最常见的几种表现和对应的解决思路:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| UnicodeDecodeError: 'utf-8' codec can't decode byte 0x... | 文件不是UTF-8编码,多半是GBK或GB2312 | 改成encoding='gbk',或用errors='replace'兜底 |
| 读出来是"锟斤拷"这类乱码 | 用错误编码解码了文件内容 | 确认原始编码,重新读;"锟斤拷"是UTF-8内容被GBK解码的经典标志 |
| 写入后打开是乱码 | 写入编码和查看工具默认编码不一致 | 写文件时显式指定encoding='utf-8' |
| csv文件用Excel打开是乱码 | 缺少BOM头,或编码不匹配 | 写入时用encoding='utf-8-sig'在文件头带BOM,Excel能正确识别 |
特别说一下utf-8-sig,这个编码会在文件开头写入BOM(字节序标记),很多Excel用户打开CSV乱码的根因就是CSV文件是纯UTF-8但Excel用本地编码去猜。用utf-8-sig写完,Excel识别就正常了。这个小技巧我在处理数据导出时几乎每回都用。
排查编码问题时,我推荐一个土办法:用十六进制看文件开头几个字节。装一个支持十六进制查看的编辑器,或者直接用Python:
with open('unknown.txt', 'rb') as f: head = f.read(16) print(head.hex(' '))如果开头是89 50 4E 47,那是PNG图片;如果是EF BB BF,说明文件带UTF-8 BOM;如果字节看起来像中文但不符合UTF-8连续规则,那很可能是GBK。看到实际字节,比瞎猜编码可靠得多。
5.2 文件被占用与PermissionError
Windows用户经常会遇到PermissionError: [Errno 13] Permission denied,最常见的场景是你写完文件想删掉它,结果系统告诉你被占用。原因几乎都是某个文件对象没关闭,特别是手动调用open()没close(),或者with块外还在引用文件对象。
排查思路很简单:看看程序里是不是有open()后没配with的地方,或者用try...except包住操作然后finally里写f.close()。还有一个是Excel或记事本正打开着这个文件,Windows下这类程序会锁文件,Python删不掉。这种是外部因素,脚本里无解,但可以先检查文件是否存在、再尝试删除,并在失败时给出明确提示:
from pathlib import Path import time p = Path('temp.txt') max_retries = 3 for i in range(max_retries): try: p.unlink() break except PermissionError: if i < max_retries - 1: time.sleep(1) else: print('文件被占用,无法删除,请关闭打开该文件的程序')还有一个容易被忽略的坑:同一个文件在with块里关闭之后,你持有的外部分片或者文件对象引用不会自动失效。比如:
f = open('data.txt', 'r') lines = f.readlines() # 这里f还活着 # 然后想删文件如果你一直不close,文件就会一直被占着。写代码时养成"打开文件就到with里去"的习惯,这类问题能消灭八成。
5.3 路径问题:相对路径、绝对路径与目录不存在的坑
路径问题在初学者里高发,而且表现五花八门。最常见的是FileNotFoundError,程序报找不到文件,但你明明看到文件就在脚本旁边。原因通常是相对路径的工作目录和你以为的不一样。
当你运行python script.py时,当前工作目录是执行命令的目录,不是脚本所在的目录。如果你在项目根目录执行python src/script.py,而script.py里写了open('data.txt'),它找的是项目根目录/data.txt,不是src/data.txt。调试这个问题时,先打印当前工作目录:
import os from pathlib import Path print(os.getcwd()) print(Path(__file__).resolve().parent)最稳妥的做法是,需要读取与脚本同目录的文件时,用脚本所在目录拼路径:
from pathlib import Path base_dir = Path(__file__).resolve().parent file_path = base_dir / 'data.txt'另一个常见坑是目录不存在。open('output/result.txt', 'w')时如果output目录不存在,会抛FileNotFoundError。遇到这种需求,要先创建目录:
from pathlib import Path out_dir = Path('output') out_dir.mkdir(parents=True, exist_ok=True) file_path = out_dir / 'result.txt'mkdir的parents=True表示如果上级目录也不存在,一口气创建;exist_ok=True表示目录已经存在时不报错。这两个参数是批量脚本里的常客,一定要记住。
5.4 读取大文件时的内存监控与实战数据
我做个了实测,有一份约1.2GB的日志文件,分别用三种方式读取,观察进程内存占用的差异。用read()读取时,Python进程内存峰值到了2.4GB,几乎是我机器的极限;用readlines()读取,内存峰值更高,到了2.7GB左右,因为不仅要存内容,还要建一个包含所有行的列表;用for line in f逐行读取时,内存峰值稳定在80MB左右,基本就是Python解释器本身的占用加一行数据。
这个差距非常直观。如果你手里有超大日志文件要处理,逐行读取是唯一靠谱的方案。如果你又要逐行读,又需要处理其中某些跨行的逻辑(比如多行合并),可以用一个缓存变量来拼接,读完一行判断是不是逻辑结束,是就处理,不是就继续读。这是"增量处理"的经典套路,比等文件全读进来再统一处理节省大量内存。
顺便说一个优化技巧:如果你明确知道只需要文件里某个区间的数据,比如只读前1000行,用itertools.islice可以优雅地限制读取范围,避免for循环从头跑到尾:
from itertools import islice with open('big_log.txt', 'r', encoding='utf-8') as f: for line in islice(f, 1000): print(line, end='')这也是for line in f的衍生玩法,学会之后很多文件截取场景会变得很清爽。
6. 实操扩展:用文件操作解决真实场景问题
6.1 批量文件整理脚本实战
我把前面提到的知识串起来,写一个批量整理下载目录的脚本。这个脚本解决的实际问题是:在没有下载分类功能的软件里,下载目录常年堆满各种文件,找东西全靠眼睛扫。目标是把图片、文档、压缩包、音频四类文件自动归档到对应子目录。
from pathlib import Path import shutil DOWNLOAD_DIR = Path(r'D:\downloads') # 换成你的实际目录 RULES = { 'images': {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}, 'docs': {'.pdf', '.doc', '.docx', '.txt', '.md', '.xlsx', '.pptx'}, 'archives': {'.zip', '.rar', '.7z', '.tar', '.gz'}, 'audios': {'.mp3', '.wav', '.flac', '.m4a'}, } target_dir = DOWNLOAD_DIR / 'sorted' for p in DOWNLOAD_DIR.iterdir(): if p.is_dir(): continue suffix = p.suffix.lower() for category, extensions in RULES.items(): if suffix in extensions: dest_dir = target_dir / category dest_dir.mkdir(parents=True, exist_ok=True) dest = dest_dir / p.name # 如果目标文件已存在,加时间戳后缀避免覆盖 if dest.exists(): dest = dest_dir / f'{p.stem}_{p.stat().st_mtime_ns}{p.suffix}' shutil.move(str(p), str(dest)) print(f'{p.name} -> {dest}') break这段代码用了pathlib的iterdir()遍历目录,用suffix判断扩展名,用mkdir(parents=True, exist_ok=True)确保目标目录存在,用stat().st_mtime_ns生成唯一后缀防止同名覆盖。整个脚本不需要额外安装任何库,Python标准库就能跑。我在自己机器上跑过很多次,几十秒就能把几百个文件分好类,比手动拖拖拽拽高效太多。
需要注意的一个细节是iterdir()遍历时dir的过滤。如果下载目录里有子目录,脚本会跳过,避免把自己创建的sorted目录再归类一遍。你还应该把sorted目录放到下载目录之外,或者多写一行排除判断,否则下回运行脚本时会把这些分类目录也扫进去然后尝试移动,那就要出乱子了。
6.2 日志监控与增量读取
另一个真实场景是日志监控。有个服务会持续往日志文件里追加内容,你需要写一个脚本每隔几秒读取新增的部分并输出到控制台。难点在于"记住上次读到的位置",而不是每次都从头读一遍。
实现思路是用文件对象的tell()和seek()来记录位置。核心代码:
from pathlib import Path def tail_follow(file_path, start_pos=0): pos = start_pos while True: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: f.seek(pos) new_data = f.read() if new_data: print(new_data, end='') pos = f.tell() time.sleep(2)这个脚本每次打开文件,seek到上次记录的位置,读取新增内容,再用tell()记住新的位置。等于实现了类似Linux下tail -f的效果。如果日志文件被外部程序滚动重写(比如按天切分),你还需要根据文件大小或者修改时间判断是不是新文件,那又是另一个层次的处理了。
这种增量读的写法在生产环境很常用,比如定时任务处理"上次处理后新增的数据",关键就是持久化保存pos,可以存在一个单独的小状态文件里,这就又回到了文件操作本身,形成一个闭环。
6.3 临时文件与异常安全的写法
处理文件时不希望中途崩溃留下脏文件,可以用tempfile模块创建临时文件,全部处理完之后再原子替换目标文件。这个"先写临时文件再改名"的思路是我处理重要配置和数据的标准姿势。
from pathlib import Path import tempfile import os target = Path('config.json') data = '{"name": "test", "version": 1}' with tempfile.NamedTemporaryFile('w', encoding='utf-8', delete=False, dir=target.parent) as tmp: tmp.write(data) tmp_path = Path(tmp.name) # 关键操作:用os.replace原子替换 os.replace(str(tmp_path), str(target))为什么用os.replace而不是先删除再重命名?因为os.replace是原子操作,要么新的内容整体生效,要么保持原样,不会出现文件内容写到一半崩溃导致目标文件损坏的情况。这个模式在很多专业工具和数据库底层都能看到类似设计,实战价值极高。如果你的代码要写入重要的配置文件、序列化数据,强烈建议用这个方式替代直接open写入。
7. 一些值得长期养成的操作习惯
文件操作写得久了,我总结出几条保命的习惯。第一条是写文件前先想清楚路径,脚本里用Path(file).resolve().parent作为基准,而不是相对当前工作目录。第二条是打开文件一律with,除非你真的需要手动控制生命周期。第三条是写入重要内容时显式指定encoding='utf-8',读入时先确认编码再动手,不确定就用二进制读加try解码兜底。
第四条是处理大文件时克制住用read()的冲动,逐行或分块读取,不要让内存在崩溃边缘试探。第五条是删除或覆盖文件前先备份或至少做一个存在性检查,特别是批量脚本,宁可多写几行保护代码,也不要在用户的数据上翻车。第六条是学会用os.replace实现原子写入,这是专业文件和玩具代码之间的一个分水岭。
这些习惯不需要一次全记住,可以先挑两条用起来,比如with和显式编码,其他的在踩过坑之后自然就长记性了。我自己就是从乱码和文件被占用两个坑里跌出来的,现在写文件操作代码,脑子里会自动过一遍:路径对不对、编码对不对、要不要原子写、有没有可能被别的进程锁住。
文件操作这块内容看起来基础,但往深了想,它其实是所有数据持久化处理的基石。数据库是文件操作的高级封装,消息队列的存储层是文件,缓存的落地也是文件。把这一层的基础打牢,后面学PDF处理、Excel操作、爬虫数据落盘、自动化办公脚本,都会顺畅很多。从零到进阶的关键不在背下多少API,而在理解"文件就是一个字节流"这个本质,以及知道每种操作带来的后果和代价。带着这个理解去写代码,踩坑的概率会小很多。