| Day 11 | 文件读写与数据持久化:open 模式、with 与编码 |

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() 能看到字符串里的换行符 \nread() 一次拿回全部内容;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 记事本程序可以正常追加保存,打开文件看到内容

报错原因修复
FileNotFoundErrorr 模式打开不存在的文件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 块内
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇