零基础入门python07:让程序记住数据------JSON文件和异常处理
一、上一篇课后练习讲解
结算器练习可以使用普通 assert:断言原价、折扣价和负配送费异常。断言表达"程序员确信必须成立的规则",而用户输入错误应该用 try/except 给出友好提示。
可执行验收答案
把测试保存为 test_bill.py,直接用 Python 运行即可:
python
from decimal import Decimal
from bill_calculator import calculate_order
assert calculate_order(Decimal("50"), 2, Decimal("5"), "normal")["total"] == Decimal("105")
"应该抛异常"的调用不能直接放在 assert 后面。正确写法是:
python
try:
calculate_order(Decimal("-1"), 1, Decimal("0"), "normal")
except ValueError as exc:
assert "价格" in str(exc)
else:
raise AssertionError("负价格没有被拒绝")
print("bill validation passed")
运行 python test_bill.py 应输出 bill validation passed;若把 except 写成裸 except,会把编程错误也吞掉,排查会更困难。
二、本篇要完成什么
把个人账目保存到 records.json,下次启动还能读取;文件损坏或金额输入错误时,程序不崩溃,并说明问题。

三、完整实现
python
from pathlib import Path
import json
DATA_FILE = Path('records.json')
def load_records() -> list[dict]:
"""读取持久化数据;文件不存在返回空列表,格式损坏则明确报错。"""
if not DATA_FILE.exists():
return []
try:
return json.loads(DATA_FILE.read_text(encoding='utf-8'))
except json.JSONDecodeError as error:
raise ValueError(f'账本文件不是合法JSON:{error}') from error
def save_records(records: list[dict]) -> None:
"""统一使用UTF-8和缩进保存,便于人工查看和版本控制。"""
DATA_FILE.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding='utf-8')
def add_record(records: list[dict], amount: float, note: str) -> None:
if amount <= 0:
raise ValueError('金额必须大于0')
records.append({'amount': amount, 'note': note.strip()})
try:
records = load_records()
amount = float(input('本次支出金额:'))
add_record(records, amount, input('备注:'))
save_records(records)
print(f'已保存,共{len(records)}条记录')
except (ValueError, OSError) as error:
print(f'操作失败:{error}')
Path 负责路径,json 负责格式,业务函数负责金额规则。异常捕获只包住可能失败的操作,不用一个巨大的 except Exception 把真正的编程错误隐藏掉。
四、运行结果和验收
第一次运行会创建 records.json;再次运行会在原列表后追加记录。手工把文件改成 {bad,程序应输出"不是合法JSON",而不是打印 traceback。课后练习:增加收入/支出 kind 字段,并在下一篇用类封装账本对象。
实战补充:JSON 记账本的读写边界
文件项目的核心不是 json.loads,而是定义文件不存在、内容损坏和保存失败时的行为。数据层负责读写,命令行层负责提示。
python
def load_records(path: Path) -> list[dict]:
if not path.exists():
return []
try:
data = json.loads(path.read_text(encoding='utf-8'))
except json.JSONDecodeError as exc:
raise ValueError('账本文件不是有效 JSON') from exc
if not isinstance(data, list):
raise ValueError('账本根节点必须是列表')
return data
def save_records(path: Path, records: list[dict]) -> None:
temp = path.with_suffix('.tmp')
temp.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding='utf-8')
temp.replace(path)
先写临时文件再替换,避免程序写到一半被终止而留下半个 JSON。金额读取后转换 Decimal,保存时再转成字符串,避免浮点污染。
验收与课后练习
首次运行创建空账本,写入两条记录,故意破坏 JSON 后得到可读错误。课后增加导出 CSV,下一篇用类封装账本生命周期。
本篇结束:完整模块文件
本节不是代码片段,而是本篇结束时该模块的完整版本。请先备份旧文件,再整体替换;替换后重新运行本篇命令和测试。阅读时重点看本篇新增的函数、事务边界和错误处理,未涉及的代码先不要自行删减。
main.py
python
from pathlib import Path
import json
path=Path('records.json')
try: records=json.loads(path.read_text('utf-8')) if path.exists() else []
except json.JSONDecodeError: records=[]
try:
amount=float(input('支出金额:'))
if amount<=0: raise ValueError('金额必须大于0')
records.append({'amount':amount,'note':input('备注:')})
path.write_text(json.dumps(records,ensure_ascii=False,indent=2),'utf-8')
except ValueError as error: print('输入错误:',error)