本节摘要:异常是程序在说"这里出问题了",崩溃是没人接话的直接后果。本节讲异常的传播机制与 try / except / else / finally 的完整语法,教你有选择地接住特定异常、必要时主动抛出,最后给轻记账装上输入校验与文件容错,用户乱敲、文件损坏都不再闪退。会处理异常,是脚本与应用的分水岭。
阅读完本节,你应当能够:
上一节末尾留了个坑:把 ledger.json 改坏后再启动,程序当场倒下,终端打出这么一串:
Traceback (most recent call last): File "main.py", line 18, in <module> ledger = load_ledger() File "main.py", line 12, in load_ledger return json.load(f) json.decoder.JSONDecodeError: Expecting ',' delimiter: line 2 column 14
新手看到这堵墙就慌,其实它是全程序最有信息量的输出。从下往上读:最底行是异常类型 JSONDecodeError 加具体原因——第 2 行第 14 列少个逗号;上面几行是"案发路径",哪个调用链走到这里的。三个信息齐了:什么错、在哪错、为什么错。异常处理要做的就是:在这堵墙砌起来之前,把该接的接住。

try: with open("ledger.json", "r", encoding="utf-8") as f: ledger = json.load(f) except FileNotFoundError: print("账本文件还不存在,从空账本开始") ledger = [] except json.JSONDecodeError as e: print(f"账本文件损坏了:{e}") print("已重置为空账本,原文件改名为备份") os.rename("ledger.json", "ledger_broken.json") ledger = []
三个要点。第一,接具体的类型,不要裸接。只写 except: 会把所有错误—including 你拼错变量名的 NameError—一并吞掉,bug 藏进地毯下面,日后排错挖都挖不出来。第二,except 可以叠多个,从具体到一般排列。第三,as e 把异常对象抓进变量,类型描述、出错位置都在里面,记日志必用它。
def read_config(path): try: f = open(path, "r", encoding="utf-8") except FileNotFoundError: print("配置缺失,用默认配置") return {} else: data = f.read() # 只在没异常时执行 f.close() return data finally: print("读取流程结束") # 无论成败都执行,常用于清理
分工背一遍就牢:try 放"可能出事"的语句;except 接住处理;else 在一切顺利时补一段(避免把不该试的代码塞进 try);finally 收尾清理,比如关闭连接。日常写 try / except 两段居多,else 和 finally 按需出现。
异常不止"被动发生",还能主动抛。函数发现调用者给的参数不合理时,与其返回一个模棱两可的值,不如当场把问题喊出来:
def add_record(ledger, item, amount): if not item: raise ValueError("用途不能为空") if amount <= 0: raise ValueError("金额必须是正数") ledger.append({"item": item, "amount": amount})
调用者因此明确知道"这函数在什么条件下会炸",可以选择接住,也可以放任它把 bug 暴露在开发阶段——开发期大声报错,运行期体面兜底,是异常使用的总原则。
最容易出事的入口是 float(input("金额:")),用户敲"abc"直接 ValueError。包一层循环校验:
def read_amount(): while True: raw = input("金额:") try: value = float(raw) except ValueError: print("金额得是数字,比如 12.5,请重输") continue if value <= 0: print("金额要大于零,请重输") continue return value # 主循环里原来的 amount = float(input("金额:")) 换成: # ledger.append({"item": item, "amount": read_amount()})
现在用户敲错,得到的是一句人话提示和重输机会,程序稳如老狗。变式一:给"删"指令的序号输入做同样校验,注意区分 ValueError(非数字)和 IndexError(序号越界)两种异常。变式二:把 read_amount 的重试上限设为三次,超过就 raise 一个自定义异常给上层决定怎么办——体会"底层收集信息、上层做决策"的分层。
程序稳了。下一节逛逛标准库——日期、命令行参数、表格导出这些需求,官方零件库多半已经备好。