凌晨三点,一栋名为“订单中心”的大楼里警报骤响——地下水管爆裂,水流正涌向机房。可这座城市并没有因此瘫痪:值班室接到警报后,迅速定位是“管道事故”类别,派出了专门的抢修队;后勤组同步封路、清理积水;第二天,市政厅还根据这次事故新增了“老旧水管专项预警”。城市照常运转,甚至比昨天更结实了一点。 你的程序就是这座城。水管爆裂是异常,抢修队是 except,封路清场是 finally,新增的专项预警是自定义异常,而警报逐级上报到能处理它的部门,则是异常传播。一个健壮的程序不是永远不出错,而是出错之后不崩盘、有预案、能恢复。本篇文章就用这座城市的应急体系做镜子,带你彻底看懂 Python 的异常处理——让程序在任何风浪面前都稳如磐石。

岗位关联

学会异常处理,你写的代码才能从“仅能运行”走向“生产级健壮”——这是 AI 工程师面对不可控的 IO、网络与模型调用时最重要的生存技能。

目录
  1. 可视化演示
  2. 异常层次结构:突发事件的分类体系
  3. try/except捕获:设立安全监测站
  4. else补充分支:平安无事的庆功会
  5. finally清理保障:不管怎样都要清场
  6. 自定义异常:城市新增的专属警报
  7. raise主动抛出:不等事故,先拉警报
  8. 异常传播机制:警报逐级上报
  9. 异常防御原则:有警必接,有案必录
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过交互式滑块调节相关系数,实时观察散点分布形态与回归线的动态变化,理解线性相关强度对数据分布的影响。

本节配有交互动效(进入视口后自动加载,约 3KB)。

线性相关与回归 蓝色发光点为样本数据,沿回归线分布 红色直线为最小二乘回归线 r = Σ(xi-x̄)(yi-ȳ) / √[Σ(xi-x̄)²·Σ(yi-ȳ)²] 调节滑块改变相关强度

异常层次结构:突发事件的分类体系

市政厅把突发事件分成自然灾害、事故灾难、公共卫生、社会安全四大类,每一类下又细分为地震、洪水、火灾、爆炸……分类的意义在于:接到警报,立刻就知道该派谁去。火灾找消防,洪水找水利。Python 的异常家族树,就是为程序准备的同一套“分类派单手册”。

Python 中所有异常的根是 BaseException,它是一切异常的祖先。在它之下,Exception 是 99% 业务异常的父类——网络错误、数值错误、键不存在,统统挂在它名下。再往下,ArithmeticError(算术异常)、LookupError(查找异常)、OSError(系统调用异常)等标准家族继续细分。这种层级带来一个实用结论:捕获父类,等于同时捕获它的所有子类。比如 except OSError 能接住 FileNotFoundError,因为文件不存在正是 OSError 家族的一员。

掌握这棵家族树,你写 except 时就知道该调哪支“应急队伍”:接火警的不要派去治水,接数值错误的也不要去接网络故障。分类越清晰,程序面对意外时的手感就越从容。

BaseException⊇Exception⊇{ArithmeticError, LookupError, OSError}\displaystyle \text{BaseException} \supseteq \text{Exception} \supseteq \{\text{ArithmeticError},\, \text{LookupError},\, \text{OSError}\}
公式表示一棵倒挂的树:BaseException 在最顶层,Exception 是它的子集,而算术、查找、系统调用等异常家族又从 Exception 分叉出去。
  1. 总纲根节点BaseException 是整棵异常树的根,连程序退出(SystemExit)、键盘中断(KeyboardInterrupt)都从它派生。
    BaseException→所有异常与系统信号的共同祖先\text{BaseException} \to \text{所有异常与系统信号的共同祖先}
  2. 主干业务层Exception 是业务代码中绝大多数的异常的父类,继承它就等于进入“可被业务代码安全捕获”的体系。
    Exception⊑BaseException\text{Exception} \sqsubseteq \text{BaseException}
  3. 具体异常家族ZeroDivisionError 属于 ArithmeticError,FileNotFoundError 属于 OSError——越往下,异常描述越精确。
    ZeroDivisionError⊑ArithmeticError⊑Exception\text{ZeroDivisionError} \sqsubseteq \text{ArithmeticError} \sqsubseteq \text{Exception}

try/except捕获:设立安全监测站

市政施工队在开挖路段四周设起安全监测站,派消防、急救两支队伍轮流值守。监测站本身不阻止事故,但它保证:一旦出事,对应的队伍会在第一时间介入。try/except 就是你在代码里设的监测站。

try 块圈起“这段代码可能出问题”的区域,except 块声明“如果出了某类问题,我来接”。你可以写 except ValueError 只接数值转换错误,也可以写 except (TypeError, ValueError) 同时接两类警报,还可以用 as e 拿到异常对象,像警察拿到现场证物一样读取详细信息。需要注意:except Exception 是“兜底网”,而裸 except: 相当于把城市里所有警报(包括系统退出信号)都接到一个万能队伍手里——看似稳妥,实则危险。

监测站的价值在于“拦得住问题、放得过正常”:代码该跑完的照常跑完,真出了意外也有队伍收拾。你会慢慢发现,程序的好坏不在顺境多顺畅,而在逆境有多稳——try/except 就是这句话的直接落地。

try 块→{无异常⇒跳过 except 块有异常 e⇒{类型匹配⇒执行对应 except类型不匹配⇒向上传播\displaystyle\text{try 块} \to\begin{cases}\text{无异常} \Rightarrow \text{跳过 except 块} \\\text{有异常 } e \Rightarrow\begin{cases}\text{类型匹配} \Rightarrow \text{执行对应 except} \\\text{类型不匹配} \Rightarrow \text{向上传播}\end{cases}\end{cases}
公式描述了异常发生后的两条出路:要么被匹配的 except 接住,要么无人认领、沿调用栈继续上抛。
  1. 监控区域启动程序进入 try 块,开始执行被监控的代码,并收集可能出现的异常集合。
    try 块→执行候选集 C\text{try 块} \to \text{执行候选集 } C
  2. 异常匹配判定当异常 e 出现时,Python 拿它和每个 except 后的类型做基于继承关系的 isinstance 匹配。
    匹配(e,T)  ⟺  e∈SubClasses(T)\text{匹配}(e, T) \iff e \in \text{SubClasses}(T)
  3. 不匹配则上抛若所有 except 都未命中,异常离开当前函数,进入调用栈的上一层继续寻找。
    未匹配⇒Prop(e)\text{未匹配} \Rightarrow \text{Prop}(e)

else补充分支:平安无事的庆功会

一场演唱会顺利结束,没有火灾也没有骚乱。演出团队会办一场内部庆功会,感谢工作人员、整理收获;而如果中途出了事,庆功会自然取消,大家转而处理事故。else 分支,就是这场“只在平安无事时举办的庆功会”。

else 块必须跟在 except 之后(如果你写了 except),它只在 try 块全程没有抛出异常时执行。这样做最大的收益是把成功路径和异常路径彻底分开:成功时该做的收尾(输出成功日志、更新成功计数、缓存结果)放在 else 里,异常时该做的补救放在 except 里,两条路径互不污染,别人读代码时一目了然。

庆功会只在不乱子时才开。写代码时多问自己一句:“这段逻辑是'顺利后的锦上添花',还是'失败时的善后处理'?”放进对的块里,程序的意图就像演唱会的节目单一样清楚。

else 块执行  ⟺  try 块全程无异常\displaystyle \text{else 块执行} \iff \text{try 块全程无异常}
公式用“当且仅当(⟺)”表达 else 的执行条件:try 块干净利落地跑完,else 才登场。
  1. 先决条件else 的前置条件是 try 块完整执行到最后一行,没有被任何异常或提前 return 打断。
    try 完成=无异常∧¬提前退出\text{try 完成} = \text{无异常} \land \lnot \text{提前退出}
  2. 互斥关系else 与 except 是互斥的两条路,不可能同时走——就像庆功会不可能和事故处理同时开场。
    else 执行∧except 执行=∅\text{else 执行} \land \text{except 执行} = \emptyset
  3. 隔离收益把成功后的额外动作放进 else,避免它们被误包进 try 而“享受”到异常捕获的保护范围。
    成功路径∩异常路径=∅\text{成功路径} \cap \text{异常路径} = \emptyset

finally清理保障:不管怎样都要清场

演唱会无论圆满结束还是因突发事故中途散场,后勤团队都得收舞台、拆灯架、还设备。清场不属于演出本身,但它永远会发生。 这就是 finally 块的写照。

finally 块无论 try 中是正常完成、抛出异常还是提前 return,都会被执行。它是资源释放的黄金位置:关闭文件、断开数据库连接、释放锁。尤其要注意它与 return 的共存关系——如果 finally 里也有 return,它会覆盖 try 里的 return 值。因此生产代码中,finally 只做清理不做返回,才是最稳妥的用法。

后勤清场虽然不参与演出本身,却是下一场演出能否按时开场的保障。同理,finally 里的清理代码看似琐碎,却决定了你的服务在长跑中会不会“资源泄露”、越跑越慢。把收尾交给 finally,你才能放心地在前台表演业务逻辑。

∀ R∈{正常完成, 异常中断, 提前 return}:finally 块∈R\displaystyle \forall\, R \in \{\text{正常完成},\, \text{异常中断},\, \text{提前 return}\}: \text{finally 块} \in R
公式用全称量词“∀(任意一条退出路径)”表达 finally 的必然性:无论走哪条路离开 try,清理动作都在路上。
  1. 全称量词∀ 表示“对任意一条退出路径”,正常、异常、return 三条路都被覆盖。
    ∀ R\forall\, R
  2. 退出路径集合列举出离开 try 块的所有可能方式,这是 finally 的适用范围。
    R∈{正常,异常,return}R \in \{\text{正常}, \text{异常}, \text{return}\}
  3. 清理动作归属finally 块的代码出现在每条路径的出口处,天然成为资源释放的挂载点。
    cleanup∈R, ∀R\text{cleanup} \in R,\, \forall R

自定义异常:城市新增的专属警报

市政厅发现:普通火灾警报不够用,暴雨来临前的泄洪预警需要专门提醒水务部门。于是他们为这座城市设计了“暴雨红色预警”——它属于“自然灾害”大类,但携带了雨量、水位等专属信息。Python 的自定义异常,就是为你的业务量身定制的专属警报。

定义方式极其简单:class MyError(Exception): pass。关键决策是继承 Exception 而非 BaseException,这样你的异常既能被 except Exception 这样的兜底网安全接住,又不会与系统退出、键盘中断这类“不该被业务吞掉”的信号混为一谈。你还可以给异常类加属性,把出错的上下文(订单号、用户 ID、失败原因)一并打包带出去——让接警报的人拿到的不只是一个“出事了”,而是“谁、在哪、因为什么出事了”。

一个大型系统往往会定义自己的“异常基类”(如 APIError),再派生出 RateLimitError、TimeoutError 等子类。这就像城市建立了一套完整的专属警报体系:每个部门听到自己的警报声就知道该动身了,而不会对着别人的警报干瞪眼。

MyError⊑Exception⊑BaseException\displaystyle \text{MyError} \sqsubseteq \text{Exception} \sqsubseteq \text{BaseException}
公式表明你的自定义异常是 Exception 家族的子类,它的定位永远在“业务异常”的体系之内。
  1. 继承关系自定义异常通过继承挂入现有异常体系,自动获得被父类捕获的能力。
    MyError⊑Exception\text{MyError} \sqsubseteq \text{Exception}
  2. 体系归属整个链条的根是 BaseException,但业务异常刻意与系统级信号在 Exception 层分界。
    Exception⊑BaseException\text{Exception} \sqsubseteq \text{BaseException}
  3. 上下文扩展自定义异常可携带附加属性,这是标准异常做不到的“专属情报”。
    MyError(context)⊃context 字段\text{MyError}(context) \supset \text{context 字段}

raise主动抛出:不等事故,先拉警报

最优秀的巡检员不会等水管爆裂才报告——他看到墙上的裂缝、渗水的痕迹,就立刻拉响警报。raise 就是程序里的“主动警报器”:不等系统自然崩溃,而是主动触发异常,把隐患摆到台面上。

raise ValueError("用户名不能为空") 是主动校验;在 except 块里写一个不带参数的 raise,则会把当前异常原样重新抛出——“我处理不了,但我也不瞒报,原封不动交给你”。raise B() from A 则用来建立异常链:当你在处理异常 A 的过程中又抛出 B,from A 会把 A 作为 __cause__ 挂在 B 身后,让排查者看到完整的“前因后果”。

主动上报听起来多此一举,却是防御式编程的核心:让错误在最早的时间、最近的层次暴露出来。就像巡检员的一道警报救下整条街,代码里每个及时的 raise,都可能帮你拦住一次线上事故。

raise(e):S正常→  e  S错误(e)\displaystyle \text{raise}(e): S_{\text{正常}} \xrightarrow{\;e\;} S_{\text{错误}}(e)
公式是一次状态迁移:raise(e) 把程序从正常状态 S 拉入携带异常对象 e 的错误状态。
  1. 触发动作raise(e) 是主动的,它不等待异常自然发生,而是由程序员在特定条件满足时按下警报。
    raise(e)\text{raise}(e)
  2. 状态迁移程序状态从正常迁移到错误态,状态中携带异常 e 的全部信息。
    S正常↦S错误(e)S_{\text{正常}} \mapsto S_{\text{错误}}(e)
  3. 原样重抛裸 raise 不改写异常,保留原有 traceback,是“不吞错”的标准动作。
    raise⇒rethrow(e当前)\text{raise} \Rightarrow \text{rethrow}(e_{当前})

异常传播机制:警报逐级上报

一线水管工发现爆管,但他只管维修末端管道,无权调动抢修队,于是他按流程上报给片区负责人;片区负责人同样处理不了,再上报给市政应急中心。警报就这样一层层向上传递,直到某个层级有对应的处置能力。异常传播,就是程序世界里的“逐级上报”。

当一个函数内抛出异常但没有被匹配的 except 接住时,Python 会把这个异常“顶回”调用它的上一层函数,再找不到就继续往上。这个向上回溯的路径,正是调用栈的逆序。直到某层有 except 接住,控制权才恢复;如果一路顶到程序入口仍无人认领,程序就终止并打印 traceback——那段从下往上的错误回溯,就是异常走过的完整路线图。

理解传播机制后,你再看 traceback 就不再恐惧——它不过是异常的一段“晋升之路”,每一行都指向一个曾经尝试处理但失败了的函数。真正危险的从来不是 traceback 本身,而是整条链上没有任何一层敢于接住它。

Prop(e,f1)={handled(f1),∃ except 匹配∈f1Prop(e,f2),f1 无匹配,向调用层传播\displaystyle\text{Prop}(e, f_1) =\begin{cases}\text{handled}(f_1), & \exists\, \text{except 匹配} \in f_1 \\\text{Prop}(e, f_2), & f_1 \text{ 无匹配,向调用层传播}\end{cases}
公式用递归定义描述传播:异常在当前帧 f₁ 找不到处理者,就递归地交给上一帧 f₂,直到被接住或耗尽整个调用栈。
  1. 当前帧检查每当异常到达一个函数帧,先看这层有没有能匹配的 except 子句。
    ∃ except∈f1\exists\, \text{except} \in f_1
  2. 就地处理匹配成功则异常被处置,后续代码恢复执行,传播到此为止。
    handled(f1)\text{handled}(f_1)
  3. 递归上抛匹配失败则异常带着原有信息传给调用方 f₂,继续新一轮查找。
    Prop(e,f2)\text{Prop}(e, f_2)

异常防御原则:有警必接,有案必录

市政应急中心有条铁律:最可怕的不是出事,而是瞒报。 接到警报可以处置不了、可以上报,但绝不允许压下来假装无事发生。异常防御的第一原则,同样是“别吞错”。

所谓吞错,就是 except 块里什么都不做,或只写一个 pass——异常被接住却既没记录也没补救,等于把警报压进了抽屉里。生产级的做法是:捕获异常后至少 logging.exception(e) 留下完整现场,能处理就处理,处理不了就裸 raise 交还给上层。另一对重要的方法论是 EAFP(Easier to Ask for Forgiveness than Permission,先请求谅解而非先求许可)与 LBYL(Look Before You Leap,先看再跳):Python 社区更偏爱 EAFP,即直接用 try/except 执行操作、出错再补救,而不是先做一堆类型检查——因为检查与使用之间的窗口期,本身就是竞态的温床。

真正健壮的城市,靠的不是“永远不出事”的神话,而是“事事有回音”的制度。每条异常都被记录在案、要么处理要么上报,你的代码库才会越跑越扎实——这是 AI 工程师从写脚本走向写系统的分水岭。

---

吞错=捕获(e)∧¬记录(e)∧¬补救(e)  ⟹  隐患扩散(e)\displaystyle\text{吞错} = \text{捕获}(e) \land \lnot \text{记录}(e) \land \lnot \text{补救}(e) \;\Longrightarrow\; \text{隐患扩散}(e)
公式定义了吞错的三个条件同时成立:捕获了异常、没留日志、没有补救,三者齐备就等于把隐患重新放回了系统。
  1. 捕获动作异常确实被 except 接住了,程序不会崩溃——这是唯一值得肯定的部分。
    捕获(e)\text{捕获}(e)
  2. 缺失双要素既没有记录(无日志),也没有补救(无重试、无降级、无上报),异常的下落不明。
    ¬记录(e)∧¬补救(e)\lnot \text{记录}(e) \land \lnot \text{补救}(e)
  3. 后果推导三重条件成立,隐患并未消失,而是以“潜伏”的形式继续留在系统里,等待下一次爆发。
    隐患扩散(e)\text{隐患扩散}(e)

AI 应用场景

LLM 接口调用的智能容错

阶段:模型推理 / 外部服务调用

解决的问题:调用大模型 API 时,网络抖动、限流(429)、网关超时(504)无法避免。传统做法是异常一出现整个任务直接崩溃,或靠人工盯着重试,既慢又脆弱。用异常处理可以做到:限流自动退避重试、超时直接上报监控、成功与失败的路径彻底隔离。

具体案例:某智能客服系统在高峰期频繁触发限流。工程师自定义了 RateLimitError 与 GatewayTimeoutError,except RateLimitError 内按指数退避重试,except GatewayTimeoutError 内裸 raise 交给上层告警,else 更新成功指标,finally 记录每次尝试日志。上线后批量任务的成功率从 78% 提升到 99.2%。

数据处理管道的脏数据容错

阶段:数据工程 / 特征工程

解决的问题:解析百万行外部 JSON/CSV 时,偶尔一行缺字段、类型错位,会让整条管道崩溃、之前的工作全部白费。用 try/except 捕获 KeyError、TypeError、ValueError,记录行号与原因后跳过脏行,让管道“带伤继续跑”。

具体案例:一个数据处理团队用 except KeyError as e 捕获缺失字段,把行号、字段名写进错误日志;else 块累计干净记录数;finally 输出本批处理的“成功/跳过/失败”三段式报告。管道不再因单行脏数据而整体重跑,处理时长从 6 小时降到 25 分钟。

Web 后端的统一异常出口

阶段:在线服务 / 工程化落地

解决的问题:业务错误散落在各个接口,每个视图都写一套 try/except,既重复又容易漏。利用异常传播机制,让业务异常自然向上抛出,再由全局异常处理器统一接住,按异常类型映射为 HTTP 状态码。

具体案例:一个推荐服务后端定义了业务异常基类和 NotFoundError、RateLimitError 等子类。视图层不再写大量 try/except,而是放心让异常向上传播;全局处理器捕获后统一转换为 404/429/500 响应,并记录结构化日志。代码量减少约四成,接口错误行为却更加一致。

Python 演示

python59 行
import timeimport random  class APIError(Exception):    """API 调用基础异常"""    pass  class RateLimitError(APIError):    """触发限流:请求太快被拒绝"""    pass  class GatewayTimeoutError(APIError):    """上游网关超时"""    pass  def mock_api(prompt):    """模拟不稳定的 LLM 接口:有概率限流或超时"""    code = random.randint(0, 10)    if code < 1:        raise GatewayTimeoutError("网关 504,上游无响应")    if code < 3:        raise RateLimitError("请求过于频繁,触发限流")    return {"text": "我是模型对{}字提示词的回应".format(len(prompt))}  def call_llm(prompt, max_retry=3):    """带重试的 LLM 调用:限流重试,超时上报,全程留日志"""    attempt = 0    last_err = None    while attempt < max_retry:        attempt += 1        try:            resp = mock_api(prompt)        except RateLimitError as e:            last_err = e            wait = 1.0 * (2 ** attempt)            print("[重试] 限流,{} 秒后第 {} 次重试".format(wait, attempt))            time.sleep(wait)        except GatewayTimeoutError:            raise        else:            print("[成功] 第 {} 次尝试成功".format(attempt))            return resp        finally:            print("[日志] attempt={} prompt_len={}".format(attempt, len(prompt)))    raise GatewayTimeoutError("重试 {} 次仍失败".format(max_retry)) from last_err  if __name__ == "__main__":    random.seed(7)    try:        result = call_llm("写一段 Python 异常处理代码")        print("最终结果:", result)    except APIError as e:        print("调用失败,已优雅降级:", e)
目的:演示如何用自定义异常、try/except/else/finally、raise 与异常链,写出面对不稳定外部接口依然健壮的调用代码。
  1. class APIError(Exception):定义业务异常基类,所有 API 错误挂在它名下,方便上层用 except APIError 一网打尽。
  2. raise GatewayTimeoutError("网关 504,上游无响应")主动抛出异常,模拟上游超时,给调用层一个明确的“原因标签”。
  3. except RateLimitError as e:精确捕获限流异常,as e 拿到异常对象,为后续重试积累上下文。
  4. except GatewayTimeoutError: raise超时不硬扛,裸 raise 原样上抛,保留完整 traceback,绝不吞错。
  5. else:只在 try 完全成功时执行,打印成功信息并返回,成功路径干净独立。
  6. finally:无论成败都打印日志,正如后勤清场,每次尝试都必须留痕。
  7. raise GatewayTimeoutError(...) from last_err用 from 把最后一次限流异常挂到新异常身后,形成可追溯的因果链。

常见误区

误区我以前觉得 `except:` 是最省心的写法,不管出什么都接住,程序永远不会崩,这不就是“健壮”吗?

正解裸 except: 不只捕获业务异常,连 KeyboardInterrupt(用户按 Ctrl+C)、SystemExit(程序退出)都会被你接走,等于把消防队派去治疗脚气,真正的紧急电话反而没人接。正确做法是至少 except Exception,最好精确到具体异常类型。

为何常见:新手阶段只关注“别崩”,看不到“吞掉系统信号”的副作用,等到需要优雅退出时才发现程序怎么都停不下来。

误区异常反正被接住了,程序也继续跑了,写不写日志有什么关系?我见过很多 `except: pass`,不都运行得好好的吗?

正解pass 等于把警报压进抽屉既不登记也不处置。隐患只是暂时被掩盖,数据错了、状态脏了、资源没释放,最终会在更远的地方以更难看的方式爆发。生产级代码在 except 中至少要 logging.exception(e),把现场完整保留。

为何常见:因为“接住后程序没崩”是即刻可见的收益,而“隐患扩散”是延迟且隐蔽的代价,人天然容易低估延迟惩罚。

误区为保险起见,我把整个函数体全部塞进 try,然后一个 `except Exception` 兜底,这样任何一行出错都能处理。

正解try 块越大,你越难说清“异常到底来自哪一步”,也容易把本不该被捕获的编程错误(如拼写错误)一并吞掉。正确做法是读代码时聚焦于那一行真正可能出错的“高风险操作”,让 try 块小到能一眼看穿。

为何常见:人面对不确定性时的本能是“圈更大的范围来获得掌控感”,但异常处理恰恰需要反向的克制——范围精确,处置才精准。

误区我直接写 `class MyError(BaseException): pass` 或者干脆 `class MyError: pass`,不也一样能 raise 吗?

正解不继承 Exception 的异常类无法被 except Exception 兜底网接住,还会与系统级信号混在一起;而完全裸的类则连异常协议都不完整。自定义异常的“身份证”是继承 Exception,这样它才能真正融入现有异常体系。

为何常见:初学者看到的是“能跑就行”的表象,看不到异常处理器按继承树匹配的机制,于是忽略了根基上的血缘关系。

误区既然 finally 总是执行,我把 return 写在 finally 里,岂不是能保证函数一定有返回值?

正解finally 里的 return 会覆盖 try 块中的 return,让你“精心计算的返回值”被莫名其妙地顶替。finally 的正确身份是“清场人员”——只负责释放资源、留下日志,绝不负责制造终值。

为何常见:finally 的“必执行”特性容易被过度解读为“适合兜底返回”,而忽略了它对既有返回路径的破坏力。

面试题

Q1. 下列关于异常分类的说法,正确的是哪一项?

单选题 · easy

  • A.Exception 类是最终基类
  • B.BaseException 类是最终基类← 正确答案
  • C.ArithmeticError 是最终基类
  • D.RuntimeError 是最终基类

答案:B。BaseException 是 Python 中一切异常与系统信号的共同祖先,Exception 只是其下负责业务异常的主干分支。就像城市的《总应急预案》覆盖所有事件类型,而“事故灾难类”只是其中一个子类。其余选项中的类都挂在 BaseException 之下,不是最终基类。

Q2. 下列哪个 except 块能成功捕获 ValueError 异常?

单选题 · easy

  • A.except BaseException← 正确答案
  • B.except KeyboardInterrupt
  • C.except ZeroDivisionError
  • D.except OSError

答案:A。因为 BaseException 是所有异常的祖先,匹配异常时按继承关系判定,所以 except BaseException 能接住任何异常。KeyboardInterrupt 与 ValueError 算是“不同大类”,ZeroDivisionError 属算术家族,OSError 属系统调用家族,都接不住 ValueError——如同火警电话接不了涝情。

Q3. try...except...else...finally 结构中的 else 块在什么时机执行?

单选题 · easy

  • A.try 无论是否正常都执行
  • B.try 抛出异常之后执行
  • C.try 未抛出异常才执行← 正确答案
  • D.finally 块执行完后才执行

答案:C。else 块仅在 try 块全程无异常时执行,与 except 块互斥。它相当于演唱会顺利结束后的庆功会:没出事才有,一出事就取消。选项 A 说的是 finally 的特征,B 说的是 except 的时机,D 时序颠倒——finally 在 else 之后才执行。

Q4. 阅读下面代码,print(f()) 的输出是什么?

单选题 · mid

  • A.打印输出结果为 1
  • B.打印输出结果为 2← 正确答案
  • C.编译器直接报语法错
  • D.什么都不打印输出

答案:B。try 中的 return 1 在把值送出去之前,会先执行 finally 块;而 finally 里的 return 2 会直接覆盖前一个返回值,所以最终输出 2。这就像后勤清场时把前台准备递出的奖杯换成了清洁记录——无论你多想把奖杯送出去,清场动作都先行一步且拥有最终话语权。

Q5. 自定义业务异常类时,最合理的做法是下列哪一项?

单选题 · mid

  • A.继承 BaseException 类
  • B.继承 Exception 类← 正确答案
  • C.直接继承 object 类
  • D.直接继承 KeyboardInterrupt

答案:B。继承 Exception 能让你的异常被 except Exception 兜底网安全捕获,同时避免与系统级信号(如退出、中断)混淆。继承 BaseException 会跳出业务异常的保护圈,继承 object 则连异常协议都不完整,继承 KeyboardInterrupt 更是张冠李戴——好比给水务局发的警报却属于火警类别。

Q6. 关于异常沿调用栈传播的过程,下列说法正确的是哪一项?

单选题 · mid

  • A.只可能在当前函数内被捕获
  • B.每个函数都会强制执行 except
  • C.逐层向上传播直到匹配← 正确答案
  • D.异常不会跨函数间传播

答案:C。异常先在当前函数帧查找匹配的 except,找不到就沿调用栈抛给上一层,如此递归直到被接住或到达程序入口。这正是“逐级上报”机制。选项 A、D 否定了跨层传播,B 错误在于 except 只有在异常到达且匹配时才执行,而不是每层强制执行。

Q7. 在 except 块中写一个不带参数的裸 raise,它的作用是下列哪一项?

单选题 · hard

  • A.重新抛出当前正在捕获的异常← 正确答案
  • B.主动抛出一个全新异常
  • C.只能在主程序顶层使用
  • D.会把当前异常静默丢弃

答案:A。裸 raise 在 except 块中会把当前正在处理的异常原样重新抛出,保留完整的 traceback——这是“不处理但也不吞错”的标准动作。它既不是构造新异常,也没有把异常丢弃,而且可以出现在任意嵌套层,应用场景恰恰是那些“处理不了就交还上层”的中间层函数。

Q8. 以下哪些属于“吞错”(异常滥用)的不良实践?

多选题 · mid

  • A.except Exception 后记录日志
  • B.使用裸 except 且直接 pass← 正确答案
  • C.try 后仅 pass 不记录信息← 正确答案
  • D.except ValueError 后记录并继续

答案:BC。吞错的三要素是“捕获了异常、没留日志、没做补救”。B 和 C 都只接住异常而不记录也不处理,等于瞒报警报;A 有日志留痕,D 有针对性处理且记录在案,都不属于吞错。记住那句话:有警必接、有案必录,处理不了就上报。

Q9. 关于 EAFP 与 LBYL 两种编程风格,下列说法正确的有哪几项?

多选题 · hard

  • A.EAFP 风格是“先尝试后认错”← 正确答案
  • B.LBYL 风格是“先检查再行动”← 正确答案
  • C.Python 社区更偏爱 LBYL
  • D.EAFP 常和 try/except 配合← 正确答案

答案:ABD。EAFP 全称 “Easier to Ask for Forgiveness than Permission“,用 try/except 先操作、出错再补救;LBYL 全称 ”Look Before You Leap”,先用 if 检查再动手。Python 社区更偏爱 EAFP,因为检查与使用之间存在竞态窗口,先检查并不保证操作一定成功——正如先看路口没车再走,仍可能被转角冲出的车撞到。

Q10. 关于 raise MyError() from original_exc 语法,下列说法正确的有哪几项?

多选题 · extreme

  • A.新异常出现在 traceback 最外层← 正确答案
  • B.旧异常以 cause 保留链条← 正确答案
  • C.两个异常会同时被 except 捕获
  • D.from 语法等价于 pass

答案:AB。raise ... from ... 会建立显式异常链:新异常作为正在抛出的异常出现在最外层,旧异常以 __cause__ 属性挂在它身后,traceback 中会显示“处理 X 时引发了 Y”的完整因果链,方便追根溯源。但 except 仍然只捕获当前抛出的新异常,而不是同时捕获两个;from 也绝非空操作。