2.2 异常处理:程序崩了怎么办


2.2 异常处理:程序崩了怎么办

本节摘要:异常是程序在说"这里出问题了",崩溃是没人接话的直接后果。本节讲异常的传播机制与 try / except / else / finally 的完整语法,教你有选择地接住特定异常、必要时主动抛出,最后给轻记账装上输入校验与文件容错,用户乱敲、文件损坏都不再闪退。会处理异常,是脚本与应用的分水岭。

本节要解决什么

阅读完本节,你应当能够:

  1. 读懂异常报告的类型、位置与原因三要素
  2. 写 try / except / else / finally 完整结构,针对不同异常分别处理
  3. 用 raise 主动抛出异常,为函数声明"用错了会怎样"
  4. 给轻记账的输入与文件读写加上容错逻辑

先看一次崩溃的解剖

上一节末尾留了个坑:把 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 列少个逗号;上面几行是"案发路径",哪个调用链走到这里的。三个信息齐了:什么错、在哪错、为什么错。异常处理要做的就是:在这堵墙砌起来之前,把该接的接住。

图 2-2 异常的传播与接住

图 2-2 异常的传播与接住

try / except:接住特定类型的异常

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 把异常对象抓进变量,类型描述、出错位置都在里面,记日志必用它。

else 与 finally:完整四段式

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 按需出现。

raise:主动掀桌也是设计

异常不止"被动发生",还能主动抛。函数发现调用者给的参数不合理时,与其返回一个模棱两可的值,不如当场把问题喊出来:

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 一个自定义异常给上层决定怎么办——体会"底层收集信息、上层做决策"的分层。

易错点清单

  • 裸 except 吞一切:至少写 Exception,最好写具体类型
  • except 里啥也不干:接住了却不处理不记录,出了问题无从查起,至少打一行日志
  • 把大段代码全塞进 try:异常来源变得模糊,try 只包真正可能出事的那几行
  • 用异常做正常流程:循环判断该用 if,别靠"猜它报错"控制逻辑

本节要点回顾

  • 异常报告三要素:类型、位置、原因,从下往上读
  • try / except 接特定异常,else 管顺利分支,finally 管清理
  • raise 主动抛出,让函数的错误契约清清楚楚
  • 轻记账防手滑完成:金额输入有校验、文件损坏有兜底
  • 原则:开发期大声报错,运行期体面兜底,永远别裸吞

程序稳了。下一节逛逛标准库——日期、命令行参数、表格导出这些需求,官方零件库多半已经备好。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U