Python(三十四) 文件与 IO 模块完整教程
目录
- 开篇:什么是 IO?
- 文件打开模式
- 文本文件读写操作
- StringIO 与 BytesIO:内存中的"文件"
- 文件与目录操作
- 路径操作
- 序列化操作:pickle 与 json
- 新手常见 IO 错误清单
一、开篇:什么是 IO?
1.1 生活化类比
想象你有一个笔记本:
| 生活中的操作 | Python 中的对应 | 属于 |
|---|---|---|
| 翻开笔记本,写一行字 | open() + write() |
输出(Output) |
| 翻开笔记本,读之前写的内容 | open() + read() |
输入(Input) |
| 合上笔记本,放回书架 | close() |
关闭文件 |
| 用脑子记一个临时号码 | 变量 | 内存(不涉及 IO) |
| 把号码写在便利贴上贴桌上 | StringIO | 内存中的"文件" |
IO = Input(输入)+ Output(输出),就是程序和"外部世界"交换数据的过程。
- 程序把数据写到硬盘上的文件 → O(输出)
- 程序从硬盘上的文件读取数据 → I(输入)
1.2 本教程学习目标
学完本教程后,你将能够:
- 用不同模式打开文件,知道什么时候用
r、w、a - 用
with语句安全地读写文本文件,不乱码 - 用
StringIO/BytesIO在内存中模拟文件操作 - 用
os和pathlib创建、删除、遍历文件和目录 - 跨平台处理文件路径,不再被
/和\困扰 - 用
json和pickle保存和恢复 Python 数据
二、文件打开模式
2.1 模式速查表
open("文件名", "模式") 的第二个参数决定了你能对文件做什么。
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 写入位置 |
|---|---|---|---|---|
"r" |
只读(默认) | 报错 FileNotFoundError |
正常打开 | 不能写 |
"r+" |
读写 | 报错 | 正常打开 | 从开头覆盖 |
"w" |
只写 | 自动创建新文件 | 清空原内容 | 从头写 |
"w+" |
写读 | 自动创建新文件 | 清空原内容 | 从头写 |
"a" |
追加写 | 自动创建新文件 | 保留原内容 | 追加到末尾 |
"a+" |
追加读写 | 自动创建新文件 | 保留原内容 | 追加到末尾 |
"x" |
排他创建 | 自动创建新文件 | 报错 FileExistsError |
从头写 |
2.2 二进制模式(加 b)
在以上模式后加 b,就变成了二进制模式,用于处理图片、视频、音频等非文本文件:
| 模式 | 含义 | 典型场景 |
|---|---|---|
"rb" |
二进制只读 | 读取图片、PDF |
"wb" |
二进制只写 | 保存图片、下载文件 |
"rb+" |
二进制读写 | 修改二进制文件 |
2.3 图解:各模式的写入行为
原文件内容: ABCDEFG
模式 w(从头覆盖):
[新内容: 123] → 文件变成: 123
模式 a(追加到末尾):
原内容: ABCDEFG → 追加后: ABCDEFG123
模式 r+(从开头覆盖,不截断):
原内容: ABCDEFG → 写入 123 → 文件变成: 123DEFG
2.4 极简示例
# 示例1:只读模式(文件必须存在)
with open("test.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# 示例2:写入模式(会自动创建文件,也会清空已有内容!)
with open("output.txt", "w", encoding="utf-8") as f:
f.write("Hello, Python!\n")
f.write("这是第二行。\n")
# 示例3:追加模式(不会清空,新内容加在末尾)
with open("output.txt", "a", encoding="utf-8") as f:
f.write("这是追加的一行。\n")
# 示例4:二进制模式(读取图片)
with open("photo.jpg", "rb") as f:
image_data = f.read()
print(f"图片大小:{len(image_data)} 字节")
新手易踩坑:
- 坑 1:用
"w"打开已有文件 → 内容会被清空! 只想修改请用"r+"或"a"- 坑 2:用
"r"打开不存在的文件 → 直接报错! 不确定文件是否存在时,先检查或用try/except- 坑 3:读写文本忘了指定
encoding="utf-8"→ Windows 上可能乱码!
课堂小练习 1
使用 "w" 模式创建一个名为 hello.txt 的文件,写入三行自我介绍,然后用 "r" 模式读出来打印。试试分别用 "w" 和 "a" 写入,观察区别。
三、文本文件读写操作
3.1 open() 函数的标准用法
# 完整语法
f = open(
file="文件名", # 文件路径(字符串)
mode="r", # 打开模式
buffering=-1, # 缓冲策略(一般不用改)
encoding=None, # 编码格式(文本模式专用)
errors=None, # 编码错误处理
newline=None, # 换行符处理
closefd=True, # 是否关闭底层文件描述符
)
日常最常用的写法只需要前三个参数:
f = open("data.txt", "r", encoding="utf-8")
3.2 with 语句:自动关闭文件的"保镖"
为什么必须用 with?
# 不用 with 的写法(不推荐!容易忘记关闭)
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
f.close() # 容易忘记写这行!
# 如果 read() 中间出错,close() 根本不会执行 → 资源泄漏
# 用 with 的写法(推荐!自动关闭)
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# 出了 with 块,文件自动关闭,即使中间出错也会关
with 语句的好处:像请了个"保镖",无论代码正常执行还是中途出错,"保镖"都会帮你把文件关上。
3.3 读取文件的方法
with open("poem.txt", "r", encoding="utf-8") as f:
# 方法1:一次性读取全部内容(适合小文件)
all_text = f.read()
print("=== read() 全部内容 ===")
print(all_text)
# 每次 open 有独立的文件指针,推荐分开写
with open("poem.txt", "r", encoding="utf-8") as f:
# 方法2:按行读取,返回列表(每行是一个元素)
lines = f.readlines()
print("=== readlines() 按行 ===")
for line in lines:
print(line, end="") # line 已含 \n,所以 end=""
with open("poem.txt", "r", encoding="utf-8") as f:
# 方法3:逐行读取(最省内存,适合大文件!)
print("=== 逐行遍历 ===")
for line in f:
print(line.strip()) # strip() 去掉首尾空白和换行
with open("poem.txt", "r", encoding="utf-8") as f:
# 方法4:只读一行
first_line = f.readline()
print(f"第一行:{first_line.strip()}")
3.4 写入文件的方法
content = """静夜思
床前明月光,
疑是地上霜。
举头望明月,
低头思故乡。"""
# 写入文件
with open("静夜思.txt", "w", encoding="utf-8") as f:
f.write(content) # 写入字符串
# f.writelines(["行1\n", "行2\n"]) # 写入字符串列表
# 验证:读出来看看
with open("静夜思.txt", "r", encoding="utf-8") as f:
print(f.read())
3.5 中文编码处理(重要!)
# 场景:在一个文件中写入中文
# 错误做法(Windows 上可能乱码!)
with open("test.txt", "w") as f: # 没指定 encoding!
f.write("你好,世界!")
# 正确做法
with open("test.txt", "w", encoding="utf-8") as f:
f.write("你好,世界!")
# 读取时的编码必须和写入时一致
with open("test.txt", "r", encoding="utf-8") as f:
print(f.read())
# 文件来自不确定的来源?尝试常见编码
encodings_to_try = ["utf-8", "gbk", "gb2312", "latin-1"]
for enc in encodings_to_try:
try:
with open("unknown.txt", "r", encoding=enc) as f:
print(f"使用 {enc} 编码读取成功:")
print(f.read()[:100]) # 只打前100个字符
break
except (UnicodeDecodeError, FileNotFoundError):
print(f"使用 {enc} 编码失败,尝试下一个...")
新手易踩坑:
- 坑 1:读取大文件用
read()→ 内存爆炸! 大文件请用for line in f逐行读- 坑 2:写完文件忘了
f.close()→ 数据可能没真正写入磁盘! 用with自动关闭- 坑 3:Windows 上写中文不给
encoding="utf-8"→ 默认 GBK,跨平台乱码!- 坑 4:
read()读到的是字符串,readlines()读到的是列表,类型不一样!
课堂小练习 2
用 with 语句写一首你喜欢的诗到 my_poem.txt,然后用三种不同的读取方式(read()、readlines()、逐行遍历)分别读出来并打印。观察它们返回的数据类型有什么不同。
四、StringIO 与 BytesIO:内存中的"文件"
4.1 什么是"内存中的文件"?
生活类比:磁盘文件就像写在纸上的笔记(持久保存,读起来慢),StringIO 就像在脑海里默念(存在内存中,读写极快,但关机就没了)。
- 磁盘文件:数据存硬盘,速度慢,但持久保存
- StringIO:数据存内存,速度快,程序关了就没,用于临时处理字符串
- BytesIO:同 StringIO,但处理的是二进制数据(bytes)
4.2 为什么需要它们?
| 场景 | 说明 |
|---|---|
| 接口测试 | 要测一个接受"文件对象"的函数,但不想真的创建磁盘文件 |
| 临时拼接数据 | 把多个字符串拼成"类文件"对象,统一传给下游处理 |
| 图片处理 | 用 PIL/Pillow 处理图片后,不存硬盘直接传到网上 |
| 单元测试 | mock 一个文件对象,不产生磁盘垃圾 |
4.3 StringIO 示例
from io import StringIO
# === 写入 StringIO ===
sio = StringIO()
sio.write("第一行内容\n")
sio.write("第二行内容\n")
sio.write("第三行内容\n")
# 获取当前写入的全部内容
print("=== 获取 StringIO 中的内容 ===")
print(sio.getvalue()) # 获取全部已写入的字符串
# 此时指针在末尾
# === 从 StringIO 读取 ===
sio.seek(0) # 把指针移到开头,否则读不到内容!
print("=== 逐行读取 ===")
for line in sio:
print(line.strip())
sio.close() # 用完关闭(虽然程序结束时也会自动释放)
# === 从已有字符串创建 StringIO ===
text = "Hello\nWorld\nPython"
sio2 = StringIO(text) # 直接传入初始内容
print(sio2.read()) # 可以直接读
sio2.close()
关键点:seek(0) 把"光标"移到开头,相当于翻书翻回第一页。写入后不 seek 就读不到东西,因为光标在末尾。
4.4 BytesIO 示例
from io import BytesIO
# === 写入二进制数据 ===
bio = BytesIO()
bio.write("你好".encode("utf-8")) # 把字符串编码成 bytes 再写入
bio.write(b" World") # 直接写入 bytes(注意前缀 b)
bio.seek(0)
data = bio.read()
print(f"二进制数据:{data}")
print(f"解码后:{data.decode('utf-8')}")
bio.close()
# === 处理图片(模拟:下载后不存盘直接处理) ===
# 假设 image_bytes 是从网络下载的图片数据
image_bytes = b'\x89PNG\r\n...' # 模拟的 PNG 二进制数据
bio2 = BytesIO(image_bytes) # 从二进制数据创建 BytesIO
# 可以直接传给 PIL 等库:
# from PIL import Image
# img = Image.open(bio2)
bio2.close()
4.5 StringIO vs BytesIO vs 磁盘文件 对比
| 特性 | 磁盘文件 | StringIO | BytesIO |
|---|---|---|---|
| 存储位置 | 硬盘 | 内存 | 内存 |
| 速度 | 慢 | 极快 | 极快 |
| 持久性 | 程序关了还在 | 程序关了消失 | 程序关了消失 |
| 数据类型 | 文本或二进制 | 字符串(str) | 二进制(bytes) |
| 适用场景 | 持久保存、大文件 | 临时文本处理、测试 | 临时二进制处理、测试 |
新手易踩坑:
- 坑 1:写完 StringIO 不
seek(0)就直接read()→ 读到空字符串! 因为光标在末尾- 坑 2:把
str直接写入 BytesIO → 报错! 需要先.encode('utf-8')转成 bytes- 坑 3:忘记
close()→ 内存不会被立即回收(虽然最终会被垃圾回收,但好习惯是主动关闭)
课堂小练习 3
创建一个 StringIO 对象,写入三句话,然后用 seek(0) 回到开头,逐行读取并打印。再试试不 seek 直接读,观察结果。用 BytesIO 做一个相同的练习(需要把字符串 encode 成 bytes)。
五、文件与目录操作
5.1 os 模块:传统老大哥
os 模块提供了操作系统级别的文件/目录操作函数。
import os
# === 文件操作 ===
# 重命名文件
os.rename("old_name.txt", "new_name.txt")
# 删除文件(不存在会报错,需先检查)
if os.path.exists("temp.txt"):
os.remove("temp.txt")
else:
print("文件不存在,无需删除")
# 检查文件/目录是否存在
print(os.path.exists("data.txt")) # 是否存在
print(os.path.isfile("data.txt")) # 是否存在且是文件
print(os.path.isdir("my_folder")) # 是否存在且是目录
# 获取文件大小(字节)
size = os.path.getsize("data.txt")
print(f"文件大小:{size} 字节")
# === 目录操作 ===
# 创建单个目录(父目录不存在会报错)
os.mkdir("new_folder")
# 递归创建多层目录(推荐!父目录不存在自动创建)
os.makedirs("a/b/c/d", exist_ok=True) # exist_ok=True:已存在不报错
# 列出目录内容(只返回名字)
items = os.listdir(".") # "." 表示当前目录
print("当前目录内容:")
for item in items:
print(f" {item}")
# 递归遍历目录树
print("\n=== 递归遍历 ===")
for root, dirs, files in os.walk("."):
# root: 当前目录路径
# dirs: 当前目录下的子目录列表
# files: 当前目录下的文件列表
for file in files:
print(os.path.join(root, file))
# 删除空目录(目录不为空会报错)
os.rmdir("empty_folder")
# 获取当前工作目录
print(f"当前工作目录:{os.getcwd()}")
# 切换工作目录
# os.chdir("another_folder")
5.2 pathlib 模块:现代新秀(Python 3.4+ 推荐)
pathlib 用面向对象的方式处理路径,代码更直观、更易读。
from pathlib import Path
# === 创建 Path 对象 ===
p = Path("data") / "subfolder" / "file.txt" # 用 / 拼接路径!
print(p) # data/subfolder/file.txt (Linux) 或 data\subfolder\file.txt (Windows)
# === 文件操作 ===
# 检查
print(p.exists()) # 是否存在
print(p.is_file()) # 是否是文件
print(p.is_dir()) # 是否是目录
# 获取属性
print(p.name) # "file.txt"(文件名)
print(p.stem) # "file"(不含后缀)
print(p.suffix) # ".txt"(后缀)
print(p.parent) # data/subfolder(父目录)
print(p.stat().st_size) # 文件大小(字节)
# 读取和写入(直接一行搞定!)
# Path("hello.txt").write_text("你好,Pathlib!", encoding="utf-8")
# content = Path("hello.txt").read_text(encoding="utf-8")
# === 目录操作 ===
# 创建目录(exist_ok=True 表示已存在不报错)
Path("my_project/data").mkdir(parents=True, exist_ok=True)
# 遍历目录
print("当前目录内容:")
for item in Path(".").iterdir():
if item.is_file():
print(f" [文件] {item.name}")
elif item.is_dir():
print(f" [目录] {item.name}")
# 通配符匹配(类似正则但更简单)
print("\n所有 .txt 文件:")
for txt_file in Path(".").glob("*.txt"):
print(f" {txt_file}")
# 递归匹配(** 表示任意层级)
print("\n所有 .py 文件(递归):")
for py_file in Path(".").rglob("*.py"):
print(f" {py_file}")
5.3 os 操作函数 vs pathlib 对照表
| 操作 | os 写法 | pathlib 写法 |
|---|---|---|
| 判断存在 | os.path.exists("a.txt") |
Path("a.txt").exists() |
| 是否是文件 | os.path.isfile("a.txt") |
Path("a.txt").is_file() |
| 是否是目录 | os.path.isdir("d") |
Path("d").is_dir() |
| 创建多层目录 | os.makedirs("a/b/c") |
Path("a/b/c").mkdir(parents=True) |
| 列出目录 | os.listdir(".") |
Path(".").iterdir() |
| 拼接路径 | os.path.join("a", "b") |
Path("a") / "b" |
| 获取文件名 | os.path.basename(p) |
Path(p).name |
| 获取父目录 | os.path.dirname(p) |
Path(p).parent |
建议:新代码尽量用 pathlib,它更简洁、跨平台、面向对象。老代码中可能常见 os.path,需要能读懂。
新手易踩坑:
- 坑 1:
os.rmdir()删除非空目录 → 报错! 请用shutil.rmtree()删除非空目录- 坑 2:
os.mkdir()创建嵌套目录 → 报错! 改用os.makedirs()或Path.mkdir(parents=True)- 坑 3:
os.listdir()只返回名字,不包含完整路径 → 要用os.path.join(dir, name)拼出完整路径
课堂小练习 4
- 用
pathlib在当前目录下创建practice/data/logs三层目录(要求一行代码完成)。 - 列出当前目录下所有
.md文件(用glob)。 - 递归列出当前目录下所有
.py文件(用rglob)。
六、路径操作
6.1 为什么要关注跨平台路径?
| 系统 | 路径分隔符 | 示例 |
|---|---|---|
| Windows | \ |
C:\Users\小明\Documents\file.txt |
| macOS / Linux | / |
/home/小明/Documents/file.txt |
如果代码里硬编码了 \,到了 Linux 上就跑不通了。
6.2 os.path:传统路径操作
import os
# 拼接路径(自动用正确的分隔符!)
path = os.path.join("folder", "subfolder", "file.txt")
print(f"拼接结果:{path}")
# Windows: folder\subfolder\file.txt
# Linux: folder/subfolder/file.txt
# 拆分路径
dir_part = os.path.dirname("/home/user/file.txt") # /home/user
base_part = os.path.basename("/home/user/file.txt") # file.txt
name, ext = os.path.splitext("document.pdf") # ('document', '.pdf')
print(f"目录部分:{dir_part}")
print(f"文件名:{base_part}")
print(f"主名:{name},后缀:{ext}")
# 获取绝对路径
abs_path = os.path.abspath(".") # 当前目录的绝对路径
print(f"当前目录绝对路径:{abs_path}")
# 路径规范化(把 / 和 \ 统一成系统格式)
normalized = os.path.normpath("a/b/../c/./d")
print(f"规范化后:{normalized}") # a\c\d (Windows) 或 a/c/d (Linux)
# 判断路径类型
path = "data.txt"
print(os.path.exists(path)) # 是否存在
print(os.path.isfile(path)) # 是否文件
print(os.path.isdir(path)) # 是否目录
print(os.path.isabs(path)) # 是否绝对路径
6.3 pathlib:现代路径操作(推荐!)
from pathlib import Path
# 创建路径对象
home = Path.home() # 用户主目录
cwd = Path.cwd() # 当前工作目录
data = Path("data") / "subfolder" / "file.txt" # 优雅拼接
print(f"用户主目录:{home}")
print(f"当前目录:{cwd}")
print(f"拼接路径:{data}")
# === 路径信息 ===
p = Path("C:/Users/小明/Documents/report.pdf")
print(f"文件名:{p.name}") # report.pdf
print(f"不含后缀:{p.stem}") # report
print(f"后缀:{p.suffix}") # .pdf
print(f"后缀列表:{p.suffixes}") # ['.pdf']
print(f"父目录:{p.parent}") # C:\Users\小明\Documents
print(f"所有父目录:{list(p.parents)}") # 从近到远的所有父级
# === 路径转换 ===
print(f"绝对路径:{p.absolute()}") # 转绝对路径
print(f"解析符号链接:{p.resolve()}") # 绝对路径 + 解析所有符号链接
print(f"转 string:{str(p)}") # 转为普通字符串
# === 路径判断 ===
target = Path("data.txt")
print(target.exists()) # 存在?
print(target.is_file()) # 是文件?
print(target.is_dir()) # 是目录?
# === 读取和写入(pathlib 超便捷功能) ===
# 读取整个文本文件
# content = Path("config.json").read_text(encoding="utf-8")
# 写入整个文本文件
# Path("output.txt").write_text("新内容", encoding="utf-8")
# 读取二进制文件
# data = Path("image.png").read_bytes()
# 写入二进制文件
# Path("copy.png").write_bytes(data)
6.4 路径兼容最佳实践
from pathlib import Path
# 不要这样写(不跨平台):
# path = "data\images\photo.jpg" # Windows only!
# 这样写(跨平台,推荐):
path = Path("data") / "images" / "photo.jpg"
# 或者:
# path = Path("data", "images", "photo.jpg")
# 构建输出路径
input_file = Path("data") / "raw" / "input.csv"
output_dir = Path("output") / "results"
output_dir.mkdir(parents=True, exist_ok=True) # 确保目录存在
output_file = output_dir / "processed.csv"
新手易踩坑:
- 坑 1:Windows 路径写
"C:\Users\小明"→\U被当成 Unicode 转义!用"C:\\Users\\小明"或原始字符串r"C:\Users\小明"或直接用/- 坑 2:用
+拼路径 →"data" + "/" + "file.txt"在 Windows 上是data/file.txt,可能能跑但不规范,用Path或os.path.join- 坑 3:用
Path创建的对象,传给某些老库可能需要先str(p)转成字符串
课堂小练习 5
- 用
pathlib输出当前文件的:文件名、后缀、父目录、绝对路径。 - 在代码中构建一个跨平台路径
"projects/my_app/data/cache",然后用mkdir(parents=True, exist_ok=True)创建它。
七、序列化操作:pickle 与 json
7.1 什么是序列化?
生活类比:
- 序列化(Serialize):把乐高城堡拆成一块块积木,装进盒子里(方便存储和运输)
- 反序列化(Deserialize):从盒子里拿出积木,按图纸重新拼成城堡
编程中:
- 序列化:把 Python 对象(字典、列表、自定义类)→ 转成可以存文件或网络传输的格式
- 反序列化:从文件或网络收到的数据 → 还原成 Python 对象
7.2 pickle:Python 专属序列化
pickle 可以把几乎任何 Python 对象变成字节串,只适合 Python 程序之间交换数据。
import pickle
# === 序列化(存到文件) ===
data = {
"name": "小明",
"age": 18,
"scores": [85, 90, 78],
"is_student": True
}
# 把字典写入文件
with open("student.pkl", "wb") as f: # 注意:必须用 wb 二进制模式!
pickle.dump(data, f)
# === 反序列化(从文件读回来) ===
with open("student.pkl", "rb") as f: # 注意:必须用 rb 二进制模式!
loaded_data = pickle.load(f)
print(loaded_data)
# {'name': '小明', 'age': 18, 'scores': [85, 90, 78], 'is_student': True}
print(type(loaded_data)) # <class 'dict'>
# === 序列化为内存中的 bytes(不发文件) ===
bytes_data = pickle.dumps(data)
print(f"字节长度:{len(bytes_data)}")
# 反序列化回来
restored_data = pickle.loads(bytes_data)
print(restored_data["name"]) # 小明
# === 自定义对象的序列化 ===
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Student(name='{self.name}', age={self.age})"
# 保存自定义对象
s1 = Student("小红", 20)
with open("student_obj.pkl", "wb") as f:
pickle.dump(s1, f)
# 读取自定义对象
with open("student_obj.pkl", "rb") as f:
s2 = pickle.load(f)
print(s2) # Student(name='小红', age=20)
print(f"姓名:{s2.name},年龄:{s2.age}")
7.3 json:跨语言通用序列化
json(JavaScript Object Notation)是最通用的数据交换格式,几乎所有编程语言都支持。
import json
# === 序列化(Python → JSON 字符串) ===
data = {
"name": "小明",
"age": 18,
"scores": [85, 90, 78],
"is_student": True,
"address": None
}
# 写入 JSON 文件
with open("student.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# ensure_ascii=False:中文正常显示,不转成 \uXXXX
# indent=2:格式化缩进,让文件好看
# 转成 JSON 字符串(不发文件)
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
# === 反序列化(JSON → Python) ===
# 从文件读取
with open("student.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["name"]) # 小明
# 从字符串解析
json_text = '{"name": "小红", "age": 20}'
parsed = json.loads(json_text)
print(parsed["name"]) # 小红
7.4 pickle vs json 对比
| 特性 | pickle | json |
|---|---|---|
| 数据格式 | 二进制(不可读) | 文本(人类可读) |
| 跨语言 | 不支持(仅 Python) | 支持(几乎所有语言) |
| 支持类型 | 几乎所有 Python 对象 | 仅 dict、list、str、int、float、bool、None |
| 安全性 | 不安全(可能执行恶意代码) | 安全 |
| 速度 | 快 | 较慢 |
| 使用场景 | Python 内部缓存、模型保存 | API 交互、配置文件、Web 前后端通信 |
7.5 json 序列化常见报错与解决
import json
from datetime import datetime
# 错误场景 1:序列化 datetime 对象
try:
data = {"time": datetime.now()}
json.dumps(data)
except TypeError as e:
print(f"错误:{e}")
# 错误:Object of type datetime is not JSON serializable
# 解决方案:自定义编码器
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime("%Y-%m-%d %H:%M:%S")
return super().default(obj)
data = {"time": datetime.now()}
json_str = json.dumps(data, cls=DateTimeEncoder, ensure_ascii=False)
print(json_str)
# {"time": "2026-07-01 14:30:00"}
# 错误场景 2:序列化 set/tuple 等类型
try:
json.dumps({"tags": {"python", "java"}}) # set 不能直接序列化
except TypeError as e:
print(f"错误:{e}")
# 错误:Object of type set is not JSON serializable
# 解决方案:转为 list
data = {"tags": list({"python", "java"})}
json_str = json.dumps(data, ensure_ascii=False)
print(json_str) # {"tags": ["python", "java"]}
# 错误场景 3:反序列化时 JSON 格式不对
try:
json.loads('{name: 小明}') # 键和字符串值必须用双引号!
except json.JSONDecodeError as e:
print(f"JSON 格式错误:{e}")
7.6 JSON ↔ Python 类型对照表
| JSON 类型 | Python 类型 |
|---|---|
object ({}) |
dict |
array ([]) |
list |
string |
str |
number (整数) |
int |
number (小数) |
float |
true / false |
True / False |
null |
None |
新手易踩坑:
- 坑 1:用
json.dumps()序列化含datetime、set的对象 → TypeError! 需要自定义 encoder 或转成支持的类型- 坑 2:
pickle用"w"(文本模式)打开文件 → 必须用"wb"/"rb"二进制模式!- 坑 3:JSON 中键和字符串用单引号 → JSON 标准要求双引号!
{'key': 'value'}不合法,{"key": "value"}才合法- 坑 4:
pickle.load()加载来路不明的.pkl文件 → 安全风险! pickle 可以执行任意代码- 坑 5:JSON 文件中有中文但不用
ensure_ascii=False→ 显示成\uXXXX乱码!
课堂小练习 6
- 创建一个包含 "name", "age", "hobbies" 三个字段的字典,分别用
pickle和json保存到文件,再读出来。 - 尝试用
json.dumps()序列化一个包含datetime对象的字典,观察报错,然后参考上文写出修正方案。
八、新手常见 IO 错误清单
错误 1:忘记关闭文件
# 错误写法
f = open("data.txt", "r")
content = f.read()
# 忘了 f.close()!文件一直处于打开状态
# 正确写法
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# with 自动关闭,安全又省心
错误 2:路径分隔符写死
# 错误写法(Windows only)
path = "data\images\photo.jpg" # \i 被当成转义符!
# 正确写法(跨平台)
from pathlib import Path
path = Path("data") / "images" / "photo.jpg"
# 或者
import os
path = os.path.join("data", "images", "photo.jpg")
错误 3:编码不统一
# 错误写法
with open("data.txt", "w") as f: # 写入时没指定编码
f.write("你好")
with open("data.txt", "r", encoding="gbk") as f: # 读取时用了不同编码
print(f.read()) # 乱码!
# 正确写法:写入和读取用同一种编码
with open("data.txt", "w", encoding="utf-8") as f:
f.write("你好")
with open("data.txt", "r", encoding="utf-8") as f:
print(f.read()) # 你好
错误 4:用 "w" 模式覆盖了重要数据
# 错误写法:本想往文件里加内容,结果把原内容清空了
with open("important.txt", "w") as f:
f.write("新增内容") # 原来的内容被清空了!
# 正确写法:用 "a" 追加模式
with open("important.txt", "a", encoding="utf-8") as f:
f.write("新增内容") # 追加在末尾,不丢失原内容
错误 5:读大文件用 read() 撑爆内存
# 错误写法(文件 2GB,内存只有 4GB)
with open("huge_file.log", "r") as f:
content = f.read() # 一次性读入内存,可能撑爆
# 正确写法:逐行读取
with open("huge_file.log", "r", encoding="utf-8") as f:
for line in f:
process(line) # 每次只处理一行,内存友好
错误 6:pickle 用了文本模式
# 错误写法
with open("data.pkl", "w") as f: # 文本模式!会报错
pickle.dump(data, f)
# 正确写法:pickle 必须用二进制模式
with open("data.pkl", "wb") as f:
pickle.dump(data, f)
错误 7:JSON 序列化不支持的类型
import json
# 错误写法
data = {"created_at": datetime.now(), "tags": {"a", "b"}}
json.dumps(data) # TypeError!
# 正确写法:先转换为 JSON 支持的类型
data = {
"created_at": datetime.now().isoformat(), # datetime → 字符串
"tags": list({"a", "b"}) # set → list
}
json.dumps(data)
错误 8:os.mkdir 创建嵌套目录
# 错误写法:父目录 a 不存在
os.mkdir("a/b/c") # FileNotFoundError!
# 正确写法
os.makedirs("a/b/c", exist_ok=True)
# 或
from pathlib import Path
Path("a/b/c").mkdir(parents=True, exist_ok=True)
错误 9:StringIO 读完不 seek 就再读
from io import StringIO
sio = StringIO("Hello World")
print(sio.read()) # Hello World(光标在末尾了)
print(sio.read()) # 空字符串!(光标在末尾,没内容了)
# 正确做法:seek(0) 重置光标
sio.seek(0)
print(sio.read()) # Hello World
错误 10:在循环里频繁打开关闭文件
# 错误写法:每次循环都 open/close,性能极差
for i in range(10000):
with open("log.txt", "a") as f:
f.write(f"行 {i}\n")
# 正确写法:打开一次,写多次
with open("log.txt", "a") as f:
for i in range(10000):
f.write(f"行 {i}\n")
总结
文件 IO 学习路线图:
1. 打开文件 → open("文件名", "模式", encoding="utf-8")
2. 安全读写 → with 语句自动管理资源
3. 内存临时文件 → StringIO(文本)、BytesIO(二进制)
4. 文件目录操作 → os 模块(传统)或 pathlib 模块(推荐)
5. 跨平台路径 → Path("a") / "b" / "c" 代替字符串拼接
6. 数据持久化 → json(通用)、pickle(Python 专属)
记住三句话:
- 读写文本别忘了 encoding="utf-8"
- 打开文件别忘了用 with
- 路径拼接别忘了用 Path 或 os.path.join