Python 保存 JSON 时,为什么要先写临时文件再替换
脚本把配置或计算结果保存成 JSON 时,常见写法是直接打开目标文件,再调用 json.dump()。这种写法很短,却有一个容易忽略的风险:以 w 模式打开文件会先截断旧内容。如果数据无法序列化、磁盘写入失败或进程中断,原本可用的文件可能只剩空文件或半段 JSON。
更稳妥的思路,是把“生成新内容”和“替换旧文件”分成两步:先在目标目录创建临时文件,完整写入并关闭它,最后用 os.replace() 替换目标。下面是一个适用于普通本地文件的入门示例,目标目录需要预先存在。
import json
import os
import tempfile
from pathlib import Path
def save_json(path, data):
path = Path(path)
temp_path = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=path.parent,
prefix=f".{path.name}.",
suffix=".tmp",
delete=False,
) as stream:
temp_path = Path(stream.name)
json.dump(data, stream, ensure_ascii=False, indent=2)
stream.flush()
os.fsync(stream.fileno())
os.replace(temp_path, path)
finally:
if temp_path is not None:
temp_path.unlink(missing_ok=True)
save_json("settings.json", {"theme": "dark", "说明": "示例配置"})
这里有四个值得理解的细节。
第一,临时文件放在目标目录。文件系统通常要求原子替换发生在同一文件系统内;如果先写系统临时目录,再移到另一块磁盘,就不能直接套用这个假设。os.replace() 成功时,常见本地文件系统中的读者会看到替换前或替换后的文件,而不是本次写入的半成品。
第二,先退出 with 再替换。这样文件句柄已经关闭,对 Windows 尤其有帮助。delete=False 让临时文件在关闭后仍存在,由后续替换或清理逻辑处理。
第三,flush() 和 fsync() 的职责不同。前者把 Python 缓冲区交给操作系统,后者请求同步文件内容。即便这样,也不能承诺任意存储设备、网络文件系统和断电场景都绝对安全;在部分系统上,还需要同步父目录才能进一步保证重命名元数据的持久性。
第四,失败后尽量清理临时文件。比如把 set 写入 JSON 会触发 TypeError,此时替换尚未执行,旧文件仍保留,finally 会删除本次临时文件。若清理本身失败,仍应让调用方看到异常并检查磁盘状态。
这个方法也有边界:两个进程同时写入时,通常是最后一次替换生效,它不提供事务、合并或并发锁;替换后的文件权限也可能与旧文件不同。涉及共享写入、权限保留或多文件一致性时,需要额外设计,不能只靠一次替换解决。
验证时可以先保存一个有效对象,再尝试保存包含 set 的对象,确认第二次报错后旧 JSON 仍能读取。理解这些边界,比把“原子写入”当作万能保证更有用。
本文为 AI 辅助撰写的原创技术示例,代码已在本地验证正常替换、序列化失败保留旧文件与临时文件清理三个场景。
- 点赞
- 收藏
- 关注作者
评论(0)