前言先划清范围这一篇只咬try语句本身的语法。异常是什么、异常体系怎么分层、该在哪一层捕获——那些是机制层面的问题属于另一篇的话题。这里不讨论「该不该捕获」只讨论「捕获怎么写才符合语言的规矩」。try语句的骨架是四个子句try、except、else、finally。它们各自的执行条件和相互之间的顺序几乎是 Python 里被误解得最多的语法点之一。几个典型的困惑写了好几个except为什么后面那个从来没被执行过except ... as e里的e出了块怎么就用不了了else到底什么时候执行finally是不是一定执行finally里写return会发生什么这些问题语言参考里都有白纸黑字的答案本文逐条对照讲清楚。所有语法细节均可在 Python 语言参考的「try 语句」一节核对。一、多个except的匹配顺序子类必须写在前面语言参考给出的匹配规则是try子句里一旦发生异常就会依次检查各个except子句直到找到一个匹配的为止。而「匹配」的定义是抛出异常的类等于该子句表达式求出的类或者是它的某个父类。这句话的反面同样重要父类的子句会拦住所有子类异常。所以下面这段代码里第二个分支是死代码# 适用于 Python 3.8def parse(value):try:return int(value)except Exception: # ❌ 先拦住了所有 Exception 的子类print(出错了)return Noneexcept ValueError: # 永远不会被执行到print(不是数字)return NoneValueError是Exception的子类int(abc)抛的ValueError在第一个子句就已经匹配上了第二个永远轮不到。这个坑的隐蔽之处在于代码不报错只是分支悄悄失效。正确写法是把具体类型放前面、笼统类型放最后# 适用于 Python 3.8def parse(value):try:return int(value)except ValueError: # ✅ 具体的在前print(不是数字)return Noneexcept (TypeError, OverflowError):print(类型或范围不对)return Noneexcept Exception: # ✅ 兜底放最后print(未预期的错误)return None两条配套规则没有表达式的except:子句如果出现必须放在最后它匹配任何异常。这不是风格建议是语法要求。except子句头部的那个表达式在匹配时被求值。语言参考提到一个细节如果except头部的表达式求值本身抛了异常原本的异常处理搜索会被取消转而按「整个try语句抛出这个新异常」来处理。所以别在except的类型位置上放会出错的表达式。顺带说一个版本变化Python 3.14 起捕获多种异常类型时可以省略那对括号PEP 758。也就是说except ValueError, TypeError:在 3.14 里是合法的但在更早的版本里这是语法错误必须写except (ValueError, TypeError):。代码要兼容 3.13 及以前时老老实实带括号。二、用元组匹配多种异常以及as的绑定与删除要捕获多种异常把类型放进一个元组# 适用于 Python 3.8data {items: [1, 2, 3]}try:first data[items][10]except (KeyError, IndexError) as exc:print(取不到这个元素, exc)元组里的每个元素都必须是异常类型不能是异常实例。把实例放进元组匹配时不会命中异常照样往上抛# 适用于 Python 3.8 反例把实例当类型try:int(abc)except (ValueError(自定义实例), TypeError) as exc:print(这里不会被执行)ValueError(自定义实例)是一个实例而不是类匹配规则要求的是「异常类」所以它永远不会命中。as绑定的目标出块后会被删除语言参考对这一点的表述是当异常通过as 目标被赋值后它会在except子句结束时被清除。参考里给出了它等价的翻译except E as N:foo# 等价于except E as N:try:foofinally:del N也就是说N这个名字在块结束后会被删掉。这不是疏漏而是有意的设计——异常对象身上挂着回溯信息会和栈帧构成引用环让栈帧里的局部变量一直存活到下一次垃圾回收删掉这个名字正是为了打破这个环。后果是块外再引用这个名字会抛NameError。# 适用于 Python 3.8try:int(abc)except ValueError as exc:message str(exc) # 需要的信息在块内提取出来print(message) # ✅ 可用print(exc) # ❌ NameError: name exc is not defined想在块外使用异常对象就在块内先赋给另一个名字。另外语言参考还提到在执行except子句的代码块之前异常会被存放在sys模块里可以在块内通过sys.exception()取得3.11 起提供。离开异常处理器时sys里保存的异常会恢复到之前的值。这条是给少数需要访问「当前异常」的场景用的日常写as exc就够了。Python 2 的写法对照这一段是 2 与 3 差异的重灾区用途Python 2Python 3绑定异常对象except ValueError, e:except ValueError as e:块外能否访问e能变量仍然存在不能块结束即被删除捕获多种except (ValueError, TypeError), e:except (ValueError, TypeError) as e:Python 2.7 已于 2020 年 1 月 1 日停止维护PEP 373。看到except X, e这种逗号写法的代码一律按上表改写不要直接拿过来跑——在 Python 3 里那个逗号会被解析成别的东西行为完全不是你想要的那样。三、else与finally的精细语义else只在「顺利走完且没提前跳走」时执行语言参考对else的定义有三个条件缺一不可控制流从try子句正常离开、没有异常被抛出、并且没有执行return、continue或break。最后一条容易被忽略。看这个例子# 适用于 Python 3.8def find_first_even(numbers):for n in numbers:try:if n % 2 0:break # 提前跳出else 不会执行except TypeError:continueelse:print(循环正常结束没有提前 break)return Noneprint(找到了)return nbreak一执行else就被跳过了。这正好对应try的else与「循环的else」在直觉上的一致性——两者都表示「这段流程完整走完了没有被中途打断」。还有一个关键性质else子句里抛出的异常不会被前面那些except子句处理。语言参考明确写了这一点。原因在于else的定位是「try成功之后才做的事」它的错误应当往上传播而不是被自己刚刚走完的这段逻辑回头接住。finally无论怎样都会执行finally是清理子句try子句会被执行包括其中的except和else无论中途发生了什么finally都会被执行。几个精确的行为如果有未处理的异常被暂时保存它会在finally子句结束时被重新抛出。如果finally子句自己抛出了另一个异常被保存的那个异常会被设置为新异常的上下文即挂到__context__上。如果finally里执行了return、break或continue这类「跳走」的语句被保存的异常会被丢弃。最后一条的后果很惊人下一节专门讲。执行顺序可以归纳成一张表情况执行顺序try顺利走完try→else→finallytry中抛出并被某个except捕获try到出错处→except→finallytry中抛出且无人捕获try到出错处→finally→ 异常继续向上抛try中执行了returnfinally先执行然后才真正返回else中抛出异常try→else到出错处→finally→ 异常向上抛前面的except不处理它四、finally里的return会覆盖try里的return语言参考把规则讲得很干脆函数返回值由最后执行的那条return语句决定。而finally子句总会执行所以finally里的return一定是最后执行的那一条。# 适用于 Python 3.8def foo():try:return tryfinally:return finally调用foo()得到的是finally不是try。try里那条return已经把返回值算好了但finally里又执行了一条return后者覆盖了前者。同样的机制会丢弃异常。语言参考给的例子是下面这个函数返回42而不是抛出ZeroDivisionError# 适用于 Python 3.8def f():try:1 / 0finally:return 421 / 0抛出ZeroDivisionError本来会被暂存、准备在finally结束后重新抛出但finally里执行了return按规则「被保存的异常被丢弃」于是这个除零错误凭空消失了。这是最危险的一类坑错误被无声无息地吃掉排查时连条线索都没有。break和continue有同样的效果同样会把暂存的异常丢掉。也正因为如此语言在版本间不断收紧这件事Python 3.8 之前finally子句里出现continue是非法的这是实现上的历史遗留问题。Python 3.14 起编译器会在finally块里出现return、break或continue时发出SyntaxWarningPEP 765——这是在提醒你这里很可能不是你想要的行为。结论很直接不要在finally里写return/break/continue。清理逻辑就只做清理把返回值留给try的正常出口。常见坑点1. 把父类异常写在子类前面❌except Exception:写在except KeyError:前面后者永远匹配不到。 ✅ 具体的异常类型写前面笼统的兜底写最后无表达式的except:必须放最末。2. 在except块外使用as绑定的名字❌except ValueError as e: pass之后又print(e)得到NameError。 ✅ Python 3 在块结束时会删除这个名字块内先赋给别的变量再用。3. 用 Python 2 的逗号语法❌except ValueError, e:或except (ValueError, TypeError), e:。 ✅ 一律改成except ValueError as e:/except (ValueError, TypeError) as e:Py2 已停维护。4. 以为break之后else还会执行❌try里if 条件: break还指望else里的收尾代码跑一遍。 ✅ 执行了return、break、continue中任意一个else都不会执行。5. 以为else里的异常会被本层的except接住❌ 把「结果处理」写在else里期待出错时被上面的except兜住。 ✅else的异常不会由前面的except处理会直接往上传播该兜就写在try里。6. 在finally里return❌finally: return result把try里的返回值、甚至正在传播的异常一并覆盖掉。 ✅finally只做清理关闭文件、释放锁返回值交给try或else的正常出口。7. 用finally吃掉了异常还不知道❌try里除零finally里恰好有条return异常被静默丢弃。 ✅ 记住规则finally里执行return、break或continue会丢弃暂存的异常3.14 起这还会触发SyntaxWarning。8. 在except类型位置上写会在求值时出错的东西❌ 把一次函数调用、一次字典取值放在except的类型位置它求值失败会取消原本的处理搜索。 ✅ 类型位置只放异常类或异常类的元组动态选择类型就先算好存成变量。总结语法点精确规则匹配顺序依次检查except子句直到命中子类必须写在父类前面无表达式的except:必须放在最后匹配任何异常元组捕获except (A, B):匹配 A 或 B3.14 起可省略括号PEP 758as绑定绑定到目标名块结束时被删除要在块外使用需另存else仅在顺利离开try、无异常、且未执行return/break/continue时执行其异常不被本层except处理finally总会执行暂存的异常会在其后重抛finally里跳走执行return/break/continue会丢弃暂存的异常返回值由最后执行的return决定所以finally里的return会覆盖try里的try语句的语法细节不多但每一条都有明确的后果匹配顺序错了分支会静默失效忘了as变量会被删除块外就报NameError在finally里return异常会被悄悄吃掉。把上表这几行记住写出来的try就不会再有意外的行为了。