课程进度 课程大纲 已发布 24/24 课
Python 基础
数据与集合
构建可靠的程序
使用对象建模
专业 Python
高级 Python
给值一个安全的架子
程序停止后,内存中的值就会消失。文件能让程序在下次运行时再次找到这些值——但文件存在于程序的控制范围之外。它们可能不存在、格式有误,或者只写入了一部分。
- 路径 架子在哪里
- with 打开、使用,然后关闭
- 格式 存储的文本采用什么结构
把存储视为一道边界:在程序的其他部分信任数据之前,先解码并验证数据。
构建路径并关闭文件
pathlib.Path 会按照当前操作系统的规则连接路径的各个部分:
from pathlib import Path
path = Path("data") / "sessions.json"
print(path)
print(path.name)
print(path.parent)
print(path.is_absolute())
典型的 macOS 或 Linux 输出:
data/sessions.json
sessions.json
data
False
Windows 通常使用反斜杠显示路径。相对路径取决于进程的当前工作目录,而这个目录可能与脚本所在的目录不同。应该接收或推导出一个明确的基础路径,不要靠猜测。
exists()、is_file() 和 is_dir() 回答的是不同的问题。检查结果可以帮助你做出选择,但无法保证之后打开文件一定会成功;在检查和打开这两个操作之间,文件系统可能已经发生变化。
使用 with,这样即使异常导致程序离开代码块,已打开的文件也会被关闭。一定要明确指定文本编码:
path = Path("plan.txt")
with path.open("w", encoding="utf-8") as file:
file.write("Python: 30 minutes\n")
with path.open("r", encoding="utf-8") as file:
print(file.read(), end="")
"r" 用于读取,"w" 用于替换内容,并且会先截断原有内容,"a" 则用于追加。追加模式适合相互独立的记录;但在一个完整的 JSON 文档后面再追加第二个完整的 JSON 文档,不会形成一个有效的 JSON 文档。
让 CSV 和 JSON 自己表达含义
CSV 字段可能包含逗号,因此不要使用 split(",") 解析它们。打开真正的 CSV 文件时,请使用 newline="",并让 csv 负责处理引号和换行符:
import csv
with Path("sessions.csv").open("w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(file, fieldnames=["topic", "minutes"])
writer.writeheader()
writer.writerow({"topic": "Data, files", "minutes": 40})
数据行会变成 "Data, files",40。csv.DictReader 会以字符串形式返回字段值,因此你需要转换并验证数字字段。
- dict / list Python 容器
- json.dump 编码为 JSON 文本
- json.load 解码为 Python 值
- 验证 检查是否符合约定的结构
JSON 对象会变成字典,数组会变成列表,null 会变成 None,布尔值会变成 True 或 False。字符串和数字可以自然对应。元组会被编码为数组,并在解码后变成列表;默认情况下不支持 Path、集合和任意对象。
解码后进行验证,并在边界处处理故障
即使程序需要的是一个由学习记录字典组成的列表,json.load() 也能成功解码 "hello"。请同时检查最外层的值和其中的每一项:
def validate_session(value):
if not isinstance(value, dict) or set(value) != {"topic", "minutes"}:
raise ValueError("a session needs topic and minutes")
topic = value["topic"]
minutes = value["minutes"]
if not isinstance(topic, str) or not topic.strip():
raise ValueError("topic must be non-empty text")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
return {"topic": topic.strip(), "minutes": minutes}
单独检查 bool 很重要,因为 bool 是 int 的子类。返回一个干净的字典,这样后面的代码就能收到一种可信且一致的结构。
- 缺失 也许可以返回空集合
- 格式有误 添加有用的解码上下文
- 意外情况 让权限问题和程序错误暴露出来
只有当文件不存在属于正常的初始状态时,才捕获 FileNotFoundError。如果有帮助,可以把 json.JSONDecodeError 转换为更清晰的边界错误。不要捕获所有 Exception 后悄悄返回空数据;权限问题和程序错误代表的是其他情况。
构建一个持久化的学习记录仓库
将下面这个完整的标准库项目保存为 study_repository.py:
import csv
import json
from pathlib import Path
from tempfile import TemporaryDirectory
def validate_session(value):
if not isinstance(value, dict) or set(value) != {"topic", "minutes"}:
raise ValueError("a session needs topic and minutes")
topic = value["topic"]
minutes = value["minutes"]
if not isinstance(topic, str) or not topic.strip():
raise ValueError("topic must be non-empty text")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
return {"topic": topic.strip(), "minutes": minutes}
def load_sessions(path):
try:
with path.open("r", encoding="utf-8") as file:
data = json.load(file)
except FileNotFoundError:
return []
except json.JSONDecodeError as error:
raise ValueError(f"invalid JSON at line {error.lineno}") from error
if not isinstance(data, list):
raise ValueError("session data must be a list")
return [validate_session(item) for item in data]
def save_sessions(path, sessions):
cleaned = [validate_session(item) for item in sessions]
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
try:
with temporary.open("w", encoding="utf-8") as file:
json.dump(cleaned, file, ensure_ascii=False, indent=2)
file.write("\n")
temporary.replace(path)
finally:
temporary.unlink(missing_ok=True)
def export_csv(path, sessions):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(file, fieldnames=["topic", "minutes"])
writer.writeheader()
writer.writerows(sessions)
def main():
with TemporaryDirectory() as directory:
base = Path(directory)
json_path = base / "sessions.json"
csv_path = base / "sessions.csv"
source = [
{"topic": " Python ", "minutes": 45},
{"topic": "Data, files", "minutes": 25},
]
assert load_sessions(json_path) == []
save_sessions(json_path, source)
restored = load_sessions(json_path)
export_csv(csv_path, restored)
assert restored[0]["topic"] == "Python"
assert '"Data, files",25' in csv_path.read_text(encoding="utf-8")
print(f"Round trip: {len(restored)} sessions, 70 minutes")
print("Tests passed.")
if __name__ == "__main__":
main()
输出:
Round trip: 2 sessions, 70 minutes
Tests passed.
验证会在写入之前进行。使用同一目录中的临时文件,可以大幅降低目标文件只写入一半的风险,但它并不能对崩溃、持久性、权限问题或多个写入者做出普遍保证。
三个小任务与本章交接
- 追加日记。 使用模式
"a"写入两行相互独立的内容;运行两次,并确认共有四行。说明为什么这种设计不适用于单个 JSON 文档。 - 导入 CSV。 使用
csv.DictReader,要求字段必须恰好是topic和minutes,转换分钟数,并验证每一行。测试一个包含逗号的主题。 - 保留旧文件。 保存有效的 JSON,然后尝试保存负数分钟。确认发生
ValueError,并且旧的目标文件仍然可以加载。
容易踩坑的地方:"w" 会立即截断文件,相对路径取决于工作目录,解码后的 JSON 可能具有错误的结构,而过于宽泛的异常处理可能掩盖真正的故障。
- 我可以使用
Path组合并检查路径。 - 我可以使用
with、UTF-8 和正确的文件模式。 - 我知道为什么真正的 CSV 文件要使用
newline=""。 - 我可以对应 JSON 值,并验证解码后的结构。
- 我只在边界处处理预期内的故障。
- 我的临时目录项目完成了完整的往返过程。
- 我完成了三个小任务。
接下来,你将把有效状态及其相关行为一起放入类和对象中。