1 数据持久化 —— 程序为什么会「失忆」
是什么?为什么学?
数据持久化(persistence)就是把内存里的数据保存到硬盘,让程序结束后数据依然存在。变量、列表存在内存里,程序一关就没了;文件存在硬盘上,下次运行还能读回来。学文件读写,本质就是学「把内存 ↔ 硬盘两头打通」。从今天开始,程序不再「失忆」了——保存配置、记录日志、存储用户数据,都离不开文件。
底层原理
程序运行时的变量在内存(RAM)里,断电即失;文件在硬盘(磁盘)上,持久保存。为了性能,Python 的 write() 通常先写入缓冲区(内存里的一块暂存区),等缓冲区满、文件关闭或程序退出时才真正写进硬盘——这一步叫「冲刷」(flush)。所以不 close 文件,数据可能还停在缓冲区里没落盘,这就是「close 很重要」的底层原因。反过来,open(..., "r") 读文件时,Python 从硬盘把内容读进内存。
生活类比
程序内存像手上的便签纸:写在上面随时能看,但纸一揉(程序退出)内容就没了。硬盘文件像家里的笔记本:翻开(open)→ 写几笔(write)→ 合上(close)——合上这步很重要,只写不合(不 close),墨水还停在笔尖(缓冲区),本子上可能什么都没留下。
注意事项总结
1 不 close 的后果——缓冲区没冲刷,文件可能是空的:
f = open("note2.txt", "w")
f.write("这行内容可能没写进去\n")
# 忘记调用 f.close()!程序结束时才由系统回收
f2 = open("note2.txt", "r")
print("读到的内容:", repr(f2.read()))
f2.close()
运行结果(缓冲区未冲刷时):
读到的内容: ''
repr() 把字符串的「真实样子」显示出来(空串显示为 '')。写完没有 close,缓冲区没被冲刷,立刻读可能读到空内容——不能依赖运气。这也是为什么后面要学 with——它保证一定会关闭。
2 跨「程序运行」的持久化——先写、再关、再读:
with open("note3.txt", "w", encoding="utf-8") as f:
f.write("今天是学习 Python 的第 11 天\n")
# 模拟「第二次运行程序」:重新打开文件读取
with open("note3.txt", "r", encoding="utf-8") as f:
content = f.read()
print("读回来的内容:", content, end="")
运行结果:
读回来的内容: 今天是学习 Python 的第 11 天
内容能完整读回来,说明数据已经从内存「搬」到了硬盘——这就是持久化。之后每次运行程序,都能读到上次存的数据,程序不再失忆。
易错点
- 错误写法:写完不调用
f.close()→ 问题:数据可能停在缓冲区没落盘,程序异常退出时会丢失 → 正确写法:用完立刻close(),或直接用with自动关闭。 - 错误写法:以为
write()是「写到硬盘」 → 问题:write 先写缓冲区,落盘时机由 Python 控制 → 正确写法:需要立刻落盘可f.flush(),但正常写完 close 即可。 - 错误写法:在
with块外面继续用 f 读写 → 问题:文件已关闭,报ValueError: I/O operation on closed file→ 正确写法:文件操作全部写在 with 块内部。
记忆口诀:内存是便签,硬盘是笔记;write 进缓冲,close 才落盘;不关会失忆,with 最省心。
2 open() 的模式 —— r / w / a 三种打开方式
是什么?为什么学?
open(文件名, 模式) 的第二个参数决定打开方式:"r" 只读(默认)、"w" 清空重写、"a" 末尾追加。选错模式是文件操作最常见的 bug:用 w 打开旧文件会把原有内容清空——很多新手因此弄丢过数据。
底层原理
模式字符串告诉操作系统「怎么打开这个文件句柄」:r(read)要求文件必须存在,打开后游标(读写位置指针)在文件开头,只能读;w(write)存在就清空、不存在就新建,打开后游标在开头,只能写;a(append)不清空、不存在就新建,打开后游标自动移到文件末尾,新内容接在后面。还有 r+(可读可写,不清空)、x(新建专属,已存在则报错)等变体,新手先掌握 r / w / a 就够。
生活类比
三种模式像三种使用笔记本的方式:r 是「只读」——翻开只能看,而且本子必须存在;w 是「买新本子从头写」——旧本子直接扔掉(清空),从第一页写起;a 是「在旧本子末尾接着写」——前面写过的内容原样保留,翻到最后一页往后写。
注意事项总结
1 模式可以带后缀——”wb” 二进制写入、”rb” 二进制读取:
with open("data.bin", "wb") as f:
f.write(bytes([0, 1, 2, 255])) # 写 4 个原始字节
with open("data.bin", "rb") as f:
print(list(f.read())) # 读回并转成列表
运行结果:
[0, 1, 2, 255]
b 表示二进制模式,读写的是字节(bytes)而不是字符串——图片、音频、视频等非文本文件必须用二进制模式。文本用 "w" / "r",二进制用 "wb" / "rb"。
2 写模式 w 会清空原有内容:
with open("notes.txt", "w") as f:
f.write("第一次写入\n")
with open("notes.txt", "w") as f:
f.write("第二次写入\n") # 旧内容被清空
运行结果:
第二次写入
3 追加模式 a 不清空,新内容写在文件末尾:
with open("notes.txt", "a") as f:
f.write("追加的内容\n")
运行结果:
第二次写入
追加的内容
选择口诀:从头重写、覆盖旧数据 → 用 w;保留旧数据、往后追加 → 用 a;只读不写 → 用 r。
易错点
- 错误写法:用
w打开已有重要文件 → 问题:旧内容瞬间被清空,无法恢复 → 正确写法:追加用a;确需覆盖前先备份。 - 错误写法:文本文件用
"wb"写字符串 → 问题:报TypeError: a bytes-like object is required→ 正确写法:文本用"w";二进制才用"wb"。 - 错误写法:以为
"a"也是「从头写」 → 问题:游标在末尾,新内容永远接在最后 → 正确写法:想从头重写用"w"(注意会清空)。
记忆口诀:r 只读、w 清空重写、a 末尾追加;用 w 先想清空,二进制加个 b。
3 with 语句 —— 自动关闭的「保险箱」
是什么?为什么学?
with open(...) as f: 是打开文件的标准姿势:代码块一结束,文件自动关闭——不管正常结束还是中途报错。手动 close() 容易忘、报错时还执行不到;with 一劳永逸。从今天起,写文件代码默认都用 with。
底层原理
with 语句依赖上下文管理协议(context manager protocol):open() 返回的对象实现了 __enter__ 和 __exit__ 两个特殊方法。进入 with 块时调用 __enter__(返回文件对象给 as 后面的 f);无论块内代码正常跑完还是抛出异常,退出时都会调用 __exit__——文件在 __exit__ 里被关闭。所以 with 保证「一定有始有终」,这是手动 close 永远做不到的。
生活类比
with 像酒店的自动退房:你入住时(__enter__)拿到房卡(文件对象);不管你是正常退房(代码跑完)还是被保安架走(报异常),前台都会自动帮你退房销卡(__exit__ 关闭文件)——绝不会出现「人走了房卡还在」的泄漏问题。手动 close 则像「记得退房」:忘了就麻烦了。
注意事项总结
1 with 块结束后,文件确实已自动关闭:
with open("auto.txt", "w", encoding="utf-8") as f:
f.write("with 会自动关闭文件\n")
print("在 with 块内,文件开着:", not f.closed)
print("离开 with 块后,文件已关闭:", f.closed)
运行结果:
在 with 块内,文件开着: True
离开 with 块后,文件已关闭: True
f.closed 是文件对象的属性——True 表示已关闭。with 退出时自动调用了关闭,不需要我们写一行 close。
2 块内报错,文件也会被自动关闭(异常安全):
try:
with open("error.txt", "w", encoding="utf-8") as f:
f.write("写了一半……")
raise ValueError("模拟中途出错")
except ValueError as e:
print("捕获异常:", e)
print("即使出错,文件也已自动关闭:", f.closed)
运行结果:
捕获异常: 模拟中途出错
即使出错,文件也已自动关闭: True
异常抛出的瞬间,with 的 __exit__ 依然被调用,文件关闭,然后异常才向外传播被 except 接住。手动 close 写法在这里会「跳过 close 直接抛异常」——文件就泄漏了。这就是 with 最值钱的地方。
易错点
- 错误写法:
with open(...)忘了as f→ 问题:语法错误,拿不到文件对象 → 正确写法:with open(...) as f:。 - 错误写法:在 with 块外面用 f 读文件 → 问题:报
ValueError: I/O operation on closed file→ 正确写法:读写代码写在 with 块内。 - 错误写法:手写
f = open(...)后忘了 close → 问题:文件长期占用,其他程序打不开;异常时直接泄漏 → 正确写法:一律用with自动管理。 - 错误写法:多个文件写了多个 with 嵌套 → 问题:缩进层层加深,代码难看 → 正确写法:用
with A as x, B as y:一次管理多个。
记忆口诀:with 打开自动关,正常异常都退房;enter 进、exit 出,close 从此不用写。
4 字符编码 —— 中文不乱码的秘密
是什么?为什么学?
编码(encoding)是把字符翻译成字节的「对照表」。写入和读取用不同的编码时,中文就会乱码甚至报错。utf-8 是国际通用的编码,能同时容纳中文、英文、emoji。给文件读写加 encoding="utf-8",是中文用户写文件代码的「标准动作」。
底层原理
电脑只认 0 和 1。字符串 "你好" 在内存里是 Unicode 字符,要存进文件必须编码(encode)成字节:"你好".encode("utf-8") 得到 b'\xe4\xbd\xa0\xe5\xa5\xbd'(每个汉字约 3 字节)。读文件时反过来解码(decode)还原成字符串。如果用 GBK 编码写入、UTF-8 解码读取,字节对不上号,就变成乱码或报 UnicodeDecodeError。Python 3 的默认编码是 utf-8,但显式写出来更稳妥——尤其代码要在别人电脑上跑时。
生活类比
编码像摩斯密码的对照表:同一串「滴答」声,用英文对照表翻译是 A,用中文对照表翻译可能完全不同。写入文件是「把话翻译成电报码」(encode),读取是「把电报码翻回人话」(decode)——发送和接收必须用同一张表,否则就是乱码。utf-8 就是「全球通用的那张表」。
注意事项总结
1 字符串 ↔ 字节的编码解码(文件读写的底层本质):
text = "你好"
data = text.encode("utf-8") # 编码:字符串 → 字节
print(data) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
back = data.decode("utf-8") # 解码:字节 → 字符串
print(back) # 你好
print("还原成功:", back == text) # True
运行结果:
b'\xe4\xbd\xa0\xe5\xa5\xbd'
你好
还原成功: True
encode("utf-8") 把「你好」变成 6 个字节(每个汉字 3 字节);decode("utf-8") 原样还原。文件写入 = 先 encode 再落盘,文件读取 = 先读盘再 decode。
2 编码不一致的后果——乱码与报错(错误示范):
with open("gbk_demo.txt", "w", encoding="gbk") as f:
f.write("中文用 GBK 编码保存\n") # 文件里存的是 GBK 字节
try:
with open("gbk_demo.txt", "r", encoding="utf-8") as f:
print(f.read())
except UnicodeDecodeError as e:
print("解码失败:", e)
运行结果(示例):
解码失败: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte
写入用 gbk、读取用 utf-8,字节对不上,抛 UnicodeDecodeError。工作中遇到这个报错,第一反应就是「读写编码不一致」。解决方案:统一 encoding="utf-8"。
易错点
- 错误写法:读写编码不一致(写 gbk、读 utf-8) → 问题:乱码或
UnicodeDecodeError→ 正确写法:读写都写encoding="utf-8"。 - 错误写法:写中文不指定编码(Windows 默认可能不是 utf-8) → 问题:换台电脑或换个程序读就乱码 → 正确写法:写中文一律显式
encoding="utf-8"。 - 错误写法:用
"wb"写字符串 → 问题:报TypeError: a bytes-like object is required→ 正确写法:文本用"w";要写字节先encode。 - 错误写法:读取时用
encoding="gbk"读 utf-8 文件 → 问题:乱码或报错 → 正确写法:先确认文件是什么编码写的,读侧保持一致。
记忆口诀:写前 encode,读后 decode,两侧同编码;中文加 utf-8,乱码远离你。
5 读取的三种姿势 —— read / readline / 逐行 for
是什么?为什么学?
read() 一次读全部、readline() 读一行、for line in f 逐行遍历——三种方式各有适用场景。读大文件(日志、几 GB 的数据)时用错方式会直接把内存撑爆,这是文件读取最重要的实战技巧。
底层原理
文件对象内部维护一个「游标」(读写位置),每次读取都从游标处开始、读完移动游标。read() 把剩余内容一次性读成字符串(大文件会占大量内存);readline() 只读到下一个换行符为止;for line in f 底层是惰性逐行读取——每循环一次只读一行,不会把整个文件装进内存,所以读多大的文件都只占一行的内存。另外注意:read() 读完后游标在文件末尾,再调用 read() 返回空字符串;想从头再读,用 f.seek(0) 把游标移回开头。
生活类比
三种姿势像三种读信方式:read() 是把整封信一口气读完(信短可以,信长会累);readline() 是只读第一行就停;for line in f 是一行一行地读——读完一行、丢一行,几十万行的流水账也能轻松读完,因为手里永远只有一行。
注意事项总结
1 read() 一次读全部 vs readline() 读一行:
with open("lines.txt", "r", encoding="utf-8") as f:
all_text = f.read() # 一口气读完剩余全部
print("read() 结果:", repr(all_text))
with open("lines.txt", "r", encoding="utf-8") as f:
line1 = f.readline() # 只读第一行
line2 = f.readline() # 游标已下移,再读是第二行
print("readline() 两次:", repr(line1), repr(line2))
运行结果:
read() 结果: '第一行\n第二行\n第三行\n'
readline() 两次: '第一行\n' '第二行\n'
repr() 能看到字符串里的换行符 \n。read() 一次拿回全部内容;readline() 每次只拿一行,且游标会记住位置——第二次调用自然读到第二行。
2 readlines() 一次拿回所有行组成的列表:
with open("lines.txt", "r", encoding="utf-8") as f:
rows = f.readlines() # 返回列表,每个元素一行
print("共有", len(rows), "行")
print("第 2 行是:", rows[1], end="")
运行结果:
共有 3 行
第 2 行是: 第二行
文件不大、想用下标访问某一行时很方便;但大文件慎用——整个列表也占内存,此时用逐行 for。
3 for line in f 逐行遍历:
f = open("hello.txt", "r")
for line in f: # 文件对象可以直接用 for 循环逐行遍历
print("读到一行:", line, end="")
f.close()
注意 print(line, end=""):因为每行末尾本来就带着换行符 \n,如果再用默认的 print 换行,就会出现空行。
易错点
- 错误写法:大文件用
f.read()或readlines()→ 问题:整个文件装进内存,几 GB 文件直接撑爆 → 正确写法:用for line in f逐行读。 - 错误写法:
read()之后再调read()想再读一遍 → 问题:游标已在末尾,返回空字符串 → 正确写法:先f.seek(0)移回开头,或重新 open。 - 错误写法:用
print(line)打印读到的行 → 问题:行尾自带\n,print 又加一个,出现空行 → 正确写法:print(line, end="")。 - 错误写法:逐行读时
rstrip()用错位置 → 问题:意外删掉行首空格 → 正确写法:只想去掉末尾换行用line.rstrip("\n")。
记忆口诀:read 全读、readline 单行、for 逐行最省内存;游标记位置,seek 回开头,大文件用 for。
6 动手实践:记事本程序
做一个「记事本」:用户输入多行内容,输入 exit 结束,然后保存到 mynotes.txt。程序要能继续追加,不覆盖旧笔记。
# my_notepad.py —— 简易记事本
# 用户输入内容,输入 exit 结束,内容追加保存到 mynotes.txt
def main():
print("简易记事本:逐行输入,输入 exit 结束保存")
lines = []
while True:
line = input("> ")
if line == "exit":
break
lines.append(line)
if lines:
with open("mynotes.txt", "a", encoding="utf-8") as f:
for line in lines:
f.write(line + "\n")
print(f"已追加保存 {len(lines)} 行到 mynotes.txt")
else:
print("没有输入任何内容")
if __name__ == "__main__":
main()
运行效果:
简易记事本:逐行输入,输入 exit 结束保存
> 今天学了文件读写
> 明天学 CSV 和 JSON
> exit
已追加保存 2 行到 mynotes.txt
再运行一次输入新内容,打开 mynotes.txt 会发现旧内容还在,新内容追加在后面——这就是 a 模式的威力。这个程序把 Day 4 的 while/break、Day 6 的列表 append、Day 8 的函数、Day 11 的文件追加全部串起来了。
升级挑战:试着把输入提示改成「显示当前已输入的行数」,并把保存路径改成用 os.path.join 拼到 data 文件夹(先 os.makedirs 创建目录)。
今日总结
数据持久化:write 先进缓冲区、close(或 with)才落盘,程序不再失忆
open() 的 r / w / a 三种模式:只读 / 清空重写 / 末尾追加;用 w 前先确认旧数据不要了
养成了用 with 打开文件的习惯,不再手写 close();块内报错文件也会自动关闭
编码原理:encode 把字符翻译成字节、decode 还原;读写两侧编码必须一致
中文文件读写都加了 encoding="utf-8"
会用 os.path.join / os.path.exists 处理路径,跨平台不报错
会用 read() / readline() / for line in f / readlines(),大文件用逐行 for
记事本程序可以正常追加保存,打开文件看到内容
| 报错 | 原因 | 修复 |
|---|---|---|
FileNotFoundError | 用 r 模式打开不存在的文件 | 先 os.path.exists 判断,或用 w / a 创建 |
PermissionError | 文件正被别的程序打开,或没有写入权限 | 关闭占用该文件的程序,检查路径权限 |
UnicodeDecodeError | 读取时编码和写入时不一致(如 utf-8 写的用 gbk 读) | 读写都显式传 encoding="utf-8" |
UnicodeEncodeError | 往文件写中文但没有指定支持中文的编码 | 写入时加 encoding="utf-8" |
TypeError: write() argument must be str | 把数字、列表直接交给 write() | 先转成字符串:str(数字) 或 f"{数字}" |
ValueError: I/O operation on closed file | 在 with 块外 / close 之后继续读写文件 | 文件操作全部写在 with 块内 |