第18章-文件读写


先做个实验:昨天你用 random 生成过成绩、用 datetime 记录过时间,现在把程序关掉再打开——那些数据还在吗?不在。因为数据存在内存里,程序一结束,内存就被清空,跟白板写完一擦一样。

想让数据"存得住",就要写进文件:文件存在磁盘上,程序关了它还在,下次打开程序再读出来。就像脑子记不住所有事,所以要写在笔记本上;程序记不住数据,所以要写在文件里。今天学的就是"程序怎么写笔记本、怎么翻笔记本":open 打开文件、write 写进去、read 读出来、with 用完好心把文件关上。另外还有一个中国程序员专属的坑要先打预防针:处理中文必须写 encoding="utf-8",不然全是乱码。周五的学生管理系统、第 6 周的大项目,核心都是"文件存取",今天就是打地基。

为什么需要文件:内存 vs 磁盘

先分清两个概念:

  • 内存:程序运行时数据住的地方。速度快,但一关程序就清空。
  • 磁盘 / 文件:数据长期保存的地方。慢一点,但程序关了数据还在。

所以标准流程是:程序里处理数据(内存)→ 需要长期保存时写进文件(磁盘)→ 下次运行程序时从文件读回内存。这叫数据持久化——说白了,就是让数据活过程序关闭的那一刻。

看一个对比演示:

# 情景 1:不存文件——程序关了就没了
data = ["你好", "Python"]
print("内存里的数据:", data)
print("(现在关掉程序,data 就没了)")

# 情景 2:写进文件——程序关了还在
with open("demo.txt", "w", encoding="utf-8") as f:
    f.write("你好,Python\n")

print("已经写进 demo.txt,关程序数据也还在")

输出:

内存里的数据: ['你好', 'Python']
(现在关掉程序,data 就没了)
已经写进 demo.txt,关程序数据也还在

跑完程序,去项目文件夹里找到 demo.txt,双击打开,能看到一行字:你好,Python。这就是"数据活下来了"。

三个容易绕进去的点:

  • 别把变量当保存:变量只在程序运行期间有效,关程序就丢;要保存就必须写文件(或数据库,那是后面的事)。
  • 文件是给"下次运行"用的:写文件之后,下一次运行程序时读出来,两次运行之间靠文件"接力"。
  • 文件位置别搞混:不写路径时,文件创建在程序所在文件夹(工作目录)里。文件找不到,先看这个文件夹。

open 与三种模式:打开文件的三把钥匙

操作文件前要先"打开",就像开门:open(文件名, 模式) 返回一个文件对象,之后所有操作都通过它(用变量 f 接住)。

模式有三把钥匙:

模式 英文含义 做什么 文件不存在时
"r" read 读取 报错 FileNotFoundError
"w" write 覆盖写入(会清空原内容 自动创建
"a" append 追加写入(接着末尾写) 自动创建

另外还有两个组合模式:"rb" / "wb" 二进制模式,处理图片这类文件用,本周不展开。

把三种模式串起来跑一遍:

# w:覆盖写(文件原来有内容也会被清空重写)
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("第一行内容\n")

# a:追加写(在原有内容后面接着写,不清空)
with open("notes.txt", "a", encoding="utf-8") as f:
    f.write("追加的第二行内容\n")

# r:读取
with open("notes.txt", "r", encoding="utf-8") as f:
    content = f.read()
    print(content)

输出:

第一行内容
追加的第二行内容

注意 w 的破坏性:同一个文件先 waw 会把它清空再写,a 不会清空。对应地有三个坑:

  • w 会清空文件——这是新手最大的坑:想"接着写"却用了 w,旧数据全没了。追加用 a,只有想"推倒重来"才用 w
  • 读不存在的文件用 r 会崩溃:报 FileNotFoundError。要先确认文件生成过(下周四学的 try/except 就是兜这个的)。
  • 写完不关可能丢数据:写入是"先到内存缓冲,再落盘",不及时关闭,最后那点数据可能没真正写进磁盘。

with 语句:用完好心关门

老式写法是 f = open(...),用完必须手动 f.close() 关文件。一旦忘关,轻则占用资源,重则写入的数据没真正落盘。with open(...) as f: 会在代码块结束后自动关闭文件——不管中间有没有报错,都替你关门。这是 Python 的推荐写法,不用纠结原理,记住这个固定句式就行。

对比一下老式和新式:

# 老式写法(不推荐:要记得手动 close,忘了就出问题)
f = open("notes.txt", "w", encoding="utf-8")
f.write("用老式写法写入")
f.close()          # 必须手动关

# 新式写法(推荐:with 结束自动关闭)
with open("notes.txt", "a", encoding="utf-8") as f:
    f.write("用 with 写入\n")
# 走到这里文件已经自动关好了,不需要写 close

运行这段没有输出,文件内容变成了两行。

还有个常见坑要演示——在 with 块外使用文件对象:

with open("notes.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(content)      # OK:内容已经读到变量里了

# 但如果在 with 外面再调 f.read() 会报错(文件已关闭)
# print(f.read())  # ValueError: I/O operation on closed file.

输出:

用老式写法写入用 with 写入

with 的三个讲究:

  • as f 不能省with open(...): 后面必须接 as 变量名,否则拿不到文件对象。
  • with 外面别再用 f:文件已自动关闭,再读写报 ValueError: I/O operation on closed file。要把内容先读到变量里,出了 with 再用。
  • 读写操作要缩进在 with 里面:别把 with 只当"打开"用,写入、读取这些操作都放在代码块里,缩进不能丢。

encoding="utf-8":中文不乱码的保命符

电脑存文字靠编码——文字和数字的对应表。Python 默认用系统编码,在 Windows 上处理中文文件容易乱码甚至直接报错。解决办法就一条:打开文件时永远带上 encoding="utf-8"。utf-8 是目前最通用的编码,几乎所有地方都认。

  • 写文件不带:中文可能被存成乱码。
  • 读文件不带:可能报 UnicodeDecodeError(解码失败)或读到乱码。

读、写、追加,全都写 encoding="utf-8",别偷懒。看正确姿势:

# 正确姿势:读写都带 encoding="utf-8"
with open("study.txt", "w", encoding="utf-8") as f:
    f.write("今天学习了文件读写\n")
    f.write("中文一定记得 utf-8\n")

with open("study.txt", "r", encoding="utf-8") as f:
    print(f.read())

输出:

今天学习了文件读写
中文一定记得 utf-8

(如果不带 encoding 去读刚才的文件,在 Windows 上可能报 UnicodeDecodeError: 'gbk' codec can't decode byte ...。)

编码这件事,三个地方要注意:

  • 三处编码要统一:写的时候、读的时候、以及你用记事本打开看的时候,编码不一致就乱码。
  • utf-8 不是唯一编码:网上有些老文件是 gbk 的,读的时候报错就把 "utf-8" 换成 "gbk" 试试(了解即可)。
  • 参数拼写是 encoding 不是 encode:写错报 TypeError: 'encoding' is an invalid keyword argument

读取的三种方式:整读、逐行读

读文件有几种方式,按需选:

  • f.read():一次性把整个文件读成一个大字符串。
  • f.readline():一次读一行(不常用)。
  • for line in f::一行一行遍历(最常用),文件大也不会卡死。
  • f.readlines():读成"每行一个元素的列表"。

有个细节:读出来的每行末尾都带着换行符 \n,直接打印会多一个空行,用 .strip() 去掉首尾空白(包括换行符)。

# 先准备一个文件
with open("names.txt", "w", encoding="utf-8") as f:
    f.write("小明\n小红\n小刚\n")

# 方式 1:整篇读成一个大字符串
with open("names.txt", "r", encoding="utf-8") as f:
    content = f.read()
print("read() 输出:", repr(content))   # repr 能看到换行符长什么样

# 方式 2(推荐):一行一行处理
with open("names.txt", "r", encoding="utf-8") as f:
    for line in f:
        print("读到:", line.strip())    # strip 去掉行尾的 \n

输出:

read() 输出: '小明\n小红\n小刚\n'
读到: 小明
读到: 小红
读到: 小刚

再看一眼 strip() 到底干了什么:

line = "小明\n"
print(repr(line))          # 有 \n
print(repr(line.strip()))  # 去掉后

输出:

'小明\n'
'小明'

三个读取上的坑:

  • 每行末尾有 \n:直接 print(line) 会多一个空行,处理时先 .strip()
  • read() 读一次就"读完"了:文件对象有"读指针",第二次 f.read() 拿到空字符串,想重读要重新 open。
  • 小文件循环读用 for line in f:比 f.readlines() 更省内存、写法最简单,记住这个就够用。

随堂练习

  1. 练习一(三步走,必做):把 3 行"今日学习内容"写进 study.txt,再追加 1 行,最后读出来打印。 提示:先用 w 写 3 行(记得每行末尾 \n),再用 a 追加 1 行,最后用 r + for line in f 逐行打印。

  2. 练习二(文件统计):读一个文本文件,统计它有多少行、多少个字,并打印。 提示:行数用 for line in f 循环计数(每读一行 行数 += 1);字数可以先 read()len(content) - content.count(" ")(减掉空格),或者 content.split() 拆成词列表再数。先用记事本建一个 article.txt 放几行文字再测。

  3. 练习三(升级猜数字,进阶):把昨天写的"猜数字"升级:每玩完一局,把"你猜了 N 次"的成绩追加写进 record.txt,每次运行都追加一行,最后读出来看看历史成绩。 提示:猜的次数是变量,写文件时用 f-string 拼成一行字符串;用 a 模式追加,别用 w(会把历史成绩清空)。

  4. 进阶题(多行记账小文件):让用户循环输入"今天干了什么",输入 q 结束,把输入的内容逐行写进 diary.txt(追加模式),退出后读取并打印全部内容。 提示:while True + break;每次输入后立即写入并刷新;最后再 open 读取。

报错了?让 AI 帮你查

文件报错种类多,自己排查慢,这三段提示词直接复制——

场景:文件读写报错 / 不知道怎么写

提示词:
我是 Python 初学者。想把一个列表里的 3 句话写进一个 txt 文件,
然后追加 1 句,最后读出来逐行打印。请给我代码,要求:
用 with open 写法,所有打开文件的地方都带 encoding="utf-8",
加中文注释,每行一个要点。
场景:统计文件内容

提示词:
请帮我写一个 Python 程序:读取一个 txt 文件,统计它有多少行、
多少个字(不含空格),然后打印结果。用 with open 写法,
带 encoding="utf-8",加注释,代码尽量简单,我是初学者。
场景:理解三种模式

提示词:
用大白话给我讲讲 open 文件时 w、a、r 三种模式的区别,
尤其是我什么时候该用 a 而不是 w。给我一个"会清空文件"的
反例演示。我是零基础,别讲太深。

常见报错速查

报错信息 什么意思 怎么办
FileNotFoundError: [Errno 2] No such file or directory 要读的文件不存在 检查文件名和路径拼写;用 w 模式先创建,再读
UnicodeDecodeError: 'gbk' codec can't decode byte ... 编码不匹配,读中文乱码 打开文件时加 encoding="utf-8"
UnicodeEncodeError: 'gbk' codec can't encode ... 写入中文时没指定编码 写入时也加 encoding="utf-8"
ValueError: I/O operation on closed file 在 with 块外面使用文件对象 把读写操作放进 with 块里,或先把内容存到变量
PermissionError: [Errno 13] 文件被别的程序(如记事本)占用 关掉占用文件的其他程序再运行
IsADirectoryError: [Errno 21] 把文件夹当成文件打开了 确认打开的是文件路径,不是文件夹路径

收个尾

今天的内容可以浓缩成一句固定句式,以后写文件代码直接套:

with open(文件名, 模式, encoding="utf-8") as f:——w 覆盖、a 追加、r 读取,w 会清空文件要小心;encoding="utf-8" 保中文不乱码;读文件优先 for line in f 逐行处理,每行记得 .strip()

明天学异常处理:程序"摔倒了能自己爬起来"。今天读文件报 FileNotFoundError 时程序直接崩溃——明天就能用 try/except 让它友好地提示"文件不存在"而不是当场死掉。你能想到,除了读文件,还有哪些地方随时可能"摔倒"吗?