学习前提
理解字典、JSON概念和try/except;使用独立练习目录。
本课完成后你能做什么
使用明确编码和上下文管理器保存结构化数据,检查读回内容。
先理解机制
相对路径相对于当前工作目录,不一定相对于脚本所在目录。pathlib提供路径对象,避免手工拼接不同系统的分隔符。文件写入是实际副作用,练习时使用独立目录或临时目录,不对未知路径直接覆盖。
文本文件需要编码,UTF-8适合本课中英文数据。with会在块结束时关闭文件,包括发生异常时。读文件不成功与JSON格式不正确是不同失败,应分别诊断。文件存在也不等于结构正确。
JSON支持普通值、数组与对象,不能直接完整保留类实例、函数或集合。dump写文件,dumps返回字符串;load和loads对应读取。ensure_ascii=False让文件更易读,但编码仍需要明确,不是设置一个参数就自动解决所有乱码。
写入后读回并验证字段,是简单有效的端到端检查。真实持久化还可能需要临时文件加替换、备份和并发策略,本课用TemporaryDirectory避免污染已有文件,不宣称这个示例已经解决所有崩溃恢复问题。
可运行示例
使用Python 3.11或更新版本,将代码保存为独立练习目录中的lesson.py,在该目录执行 python3 lesson.py;Windows也可使用 py lesson.py。示例只使用标准库。
import json
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "progress.json"
data = {"version": 1, "courses": ["Python", "数据库"], "done": 2}
with path.open("w", encoding="utf-8") as stream:
json.dump(data, stream, ensure_ascii=False, indent=2)
with path.open(encoding="utf-8") as stream:
loaded = json.load(stream)
if loaded.get("version") != 1 or type(loaded.get("done")) is not int:
raise ValueError("Unexpected progress format")
print(loaded["courses"][1], loaded["done"])
print(path.exists())
print("Temporary practice directory closed")
推演与验证
目录内读回数据库和2,文件存在为True,退出with后临时目录自动清理。故意把文件内容改为不合法JSON,观察解析错误;改为合法但缺version的对象,观察业务结构错误。
常见错误与边界
不要把json.load成功当成可信输入认证。大文件不适合一次全部读入;CSV与逐行处理可以根据任务选择。练习外写固定文件名前应确认覆盖策略。
动手练习
增加load_progress(path),文件不存在返回明确空结构,格式损坏返回错误而不静默覆盖原文件。
参考解法与理由
只捕获FileNotFoundError返回默认值,JSONDecodeError交给调用者处理或包装为有上下文的异常。保留原文件能帮助排错与人工恢复。
深入实验:坏JSON不能被悄悄覆盖
读取失败就返回空数组,看似让程序更稳定,却可能在下次保存时覆盖用户原数据。把“文件不存在”和“文件存在但内容损坏”分开处理。此实验只写系统临时目录,退出后自动清理,不接触真实资料。
import json
import os
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
target = Path(folder) / "progress.json"
target.write_text('{"unfinished":', encoding="utf-8")
before = target.read_bytes()
try:
json.loads(target.read_text(encoding="utf-8"))
except json.JSONDecodeError:
print("invalid JSON: original preserved")
assert target.read_bytes() == before
# A separate, explicitly chosen valid replacement:
replacement = {"version": 1, "items": []}
temporary = target.with_suffix(".tmp")
temporary.write_text(json.dumps(replacement), encoding="utf-8")
os.replace(temporary, target)
print(json.loads(target.read_text(encoding="utf-8"))["version"])
预期观察
输出invalid JSON: original preserved,然后1。第二步替换是示例里明确选择的新数据,不是异常处理自动触发的重置。
原因、取舍与适用边界
先写同目录临时文件再替换,能减少读者看见半份JSON的机会,但不等于断电后必然持久,也不解决两个进程互相覆盖的问题。真实应用还需考虑备份、权限、并发锁和平台文件系统语义。解析成功后仍要校验version和items结构;语法正确的JSON可以是数字42,并不一定是你的业务文档。
进阶练习
设计三个不同提示:首次运行没有文件、文件损坏、无权限读取。哪些情况允许自动创建初始文件?
解法与推理
仅在已约定首次运行逻辑且确认FileNotFoundError指向目标文件时初始化;JSONDecodeError保留原件并提供恢复说明,PermissionError说明位置和权限问题。不要用except Exception把三者都降级为空列表。