目 录CONTENT

文章目录

Python(三十四) 文件与 IO 模块完整教程

Python(三十四) 文件与 IO 模块完整教程

目录

  1. 开篇:什么是 IO?
  2. 文件打开模式
  3. 文本文件读写操作
  4. StringIO 与 BytesIO:内存中的"文件"
  5. 文件与目录操作
  6. 路径操作
  7. 序列化操作:pickle 与 json
  8. 新手常见 IO 错误清单

一、开篇:什么是 IO?

1.1 生活化类比

想象你有一个笔记本

生活中的操作 Python 中的对应 属于
翻开笔记本,写一行字 open() + write() 输出(Output)
翻开笔记本,读之前写的内容 open() + read() 输入(Input)
合上笔记本,放回书架 close() 关闭文件
用脑子记一个临时号码 变量 内存(不涉及 IO)
把号码写在便利贴上贴桌上 StringIO 内存中的"文件"

IO = Input(输入)+ Output(输出),就是程序和"外部世界"交换数据的过程。

  • 程序把数据到硬盘上的文件 → O(输出)
  • 程序从硬盘上的文件取数据 → I(输入)

1.2 本教程学习目标

学完本教程后,你将能够:

  1. 用不同模式打开文件,知道什么时候用 rwa
  2. with 语句安全地读写文本文件,不乱码
  3. StringIO / BytesIO 在内存中模拟文件操作
  4. ospathlib 创建、删除、遍历文件和目录
  5. 跨平台处理文件路径,不再被 /\ 困扰
  6. jsonpickle 保存和恢复 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,跨平台乱码!
  • 坑 4read() 读到的是字符串,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,需要能读懂。

新手易踩坑:

  • 坑 1os.rmdir() 删除非空目录报错! 请用 shutil.rmtree() 删除非空目录
  • 坑 2os.mkdir() 创建嵌套目录 → 报错! 改用 os.makedirs()Path.mkdir(parents=True)
  • 坑 3os.listdir() 只返回名字,不包含完整路径 → 要用 os.path.join(dir, name) 拼出完整路径

课堂小练习 4

  1. pathlib 在当前目录下创建 practice/data/logs 三层目录(要求一行代码完成)。
  2. 列出当前目录下所有 .md 文件(用 glob)。
  3. 递归列出当前目录下所有 .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,可能能跑但不规范,用 Pathos.path.join
  • 坑 3:用 Path 创建的对象,传给某些老库可能需要先 str(p) 转成字符串

课堂小练习 5

  1. pathlib 输出当前文件的:文件名、后缀、父目录、绝对路径。
  2. 在代码中构建一个跨平台路径 "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() 序列化含 datetimeset 的对象 → TypeError! 需要自定义 encoder 或转成支持的类型
  • 坑 2pickle"w"(文本模式)打开文件 → 必须用 "wb" / "rb" 二进制模式!
  • 坑 3:JSON 中键和字符串用单引号 → JSON 标准要求双引号! {'key': 'value'} 不合法,{"key": "value"} 才合法
  • 坑 4pickle.load() 加载来路不明的 .pkl 文件 → 安全风险! pickle 可以执行任意代码
  • 坑 5:JSON 文件中有中文但不用 ensure_ascii=False显示成 \uXXXX 乱码!

课堂小练习 6

  1. 创建一个包含 "name", "age", "hobbies" 三个字段的字典,分别用 picklejson 保存到文件,再读出来。
  2. 尝试用 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
0
博主关闭了当前页面的评论