学习前提

理解字典、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把三者都降级为空列表。

进一步阅读