返回专题首页

Python 专题

错误不可怕:异常处理、自定义异常与错误边界

程序一旦进入真实场景,出错不是例外,而是常态。读文件可能失败,请求可能超时,参数可能非法,数据库也可能断开连接。问题从来不是“会不会出错”,而是“出错后我们怎么处理”。

Python 专题第 12 篇 / 39 篇6 分钟

程序一旦进入真实场景,出错不是例外,而是常态。读文件可能失败,请求可能超时,参数可能非法,数据库也可能断开连接。问题从来不是“会不会出错”,而是“出错后我们怎么处理”。

这一节我们会系统梳理 Python 的异常处理方式,从 try / except / finally 到自定义异常,再到错误边界应该放在哪里,帮助你建立一种更工程化的出错处理思路。

为什么异常处理很重要?

很多初学者一开始会把异常理解成“程序坏掉了”。但更准确地说,异常是在告诉你:当前流程遇到了一个无法按正常路径继续推进的问题。

如果没有异常机制,很多错误只能通过返回特殊值来表达,比如返回 -1、空字符串或 None。这样做并不是完全不行,但很容易把真正的错误和合法结果混在一起。

异常的价值就在于,它把“正常业务返回”和“错误状态”明确分开了。

这会带来几个很现实的好处:

  • 错误不会悄悄混进正常数据流;
  • 调用方必须正面决定是否处理它;
  • 错误信息可以沿调用链逐层上抛,直到合适的位置再统一处理。

所以,异常机制的目标不是让程序“不要报错”,而是让错误能以更清晰、更可控的方式暴露出来。

try / except / else / finally 的完整用法

Python 最核心的异常处理结构,就是:

try:
    value = int("123")
except ValueError:
    print("invalid number")
else:
    print(value)
finally:
    print("done")

你可以把它拆成四个职责:

  • try:放可能出错的代码;
  • except:捕获并处理特定异常;
  • else:只有没出错时才执行;
  • finally:无论是否出错,最终都会执行。

其中最常被忽略的,是 else。它的意义不是“可有可无”,而是帮助你把“可能失败的逻辑”和“成功后继续做的逻辑”分开。这样结构会更清楚。

finally 则非常适合资源清理,比如关闭文件、释放连接、回收临时状态。只不过后面我们学 with 时会看到,很多资源管理最好交给上下文管理器,而不是手动堆很多 finally

常见内置异常类型

Python 内置异常很多,但初学阶段没必要一口气全记住。更重要的是先熟悉高频几类:

  • ValueError:值的类型对了,但内容不合法;
  • TypeError:操作或函数收到不合适的类型;
  • KeyError:字典里没有对应键;
  • IndexError:序列索引越界;
  • FileNotFoundError:文件不存在;
  • ZeroDivisionError:除数为 0。

理解这些异常时,有一个很重要的习惯:尽量捕获具体异常,而不是直接一把抓所有错误。

例如:

try:
    age = int(user_input)
except ValueError:
    print("请输入合法数字")

这比直接写裸 except: 要可靠得多。因为如果你无差别吞掉所有异常,很多本该尽早暴露的真实问题也会被掩盖掉。

如何定义自己的异常类?

当项目进入业务层后,内置异常往往已经不够表达具体语义了。比如“订单状态非法”“权限不足”“库存不足”这些问题,用一个普通 ValueError 并不直观。

这时就可以定义自己的异常类:

class OrderStatusError(Exception):
    """订单状态异常。"""

然后在业务里显式抛出:

def cancel_order(order):
    if order.status == "paid":
        raise OrderStatusError("已支付订单不能取消")

自定义异常的价值,不只是“可以起个业务名字”,更重要的是它把错误语义带进了系统结构里。调用方可以更准确地捕获、区分和处理不同类别的问题。

一个常见做法是给业务异常建立自己的层级,例如:

  • AppError
  • ValidationError
  • PermissionDeniedError
  • ResourceNotFoundError

这样整个系统的错误就会更像一套有结构的语言,而不是零散字符串。

不要滥用异常:错误边界该放在哪里?

虽然异常很有用,但它也不应该被滥用。

最常见的问题有两个。

第一,是把本来应该通过正常判断处理的分支,强行写成异常流程。比如明明一个字段缺失只是常见输入情况,却每次都靠抛异常再捕获处理,这会让代码读起来很绕。

第二,是在太底层或太零碎的地方就把异常吞掉,导致调用方根本不知道发生了什么。

更合理的思路通常是:

  • 在底层尽量抛出准确错误;
  • 在中间层适度补充上下文;
  • 在边界层统一转换成用户可理解的响应或日志。

这里的边界层,可能是:

  • CLI 工具的命令入口;
  • Web API 的路由处理层;
  • 后台任务的执行入口;
  • 顶层脚本的 main() 函数。

也就是说,异常不一定要在“出错的第一时间”被消化,而是应该在“最有能力做决策的那一层”被处理。

日志、上抛与用户提示如何分工?

工程里处理异常,通常不只是一句 except。它往往涉及三件事:

  • 要不要记录日志?
  • 要不要继续上抛?
  • 要不要给用户一个友好提示?

这三者不应该混成一团。

一个比较稳妥的原则是:

  • 日志面向开发者和运维,记录细节;
  • 上抛面向系统结构,让更高层决定怎么处理;
  • 用户提示面向最终使用者,只展示必要信息。

比如数据库连接失败时:

  • 日志里可以记完整异常栈;
  • 业务层可以把它包装成统一服务异常继续上抛;
  • 用户界面只提示“服务暂时不可用,请稍后重试”。

如果把底层异常原文直接暴露给用户,体验和安全性往往都不好;但如果完全不记日志,问题又很难排查。

所以异常处理的成熟度,往往体现在你是否把“开发者视角”和“用户视角”分开了。

总结与预告

这一节我们把异常处理从语法层面推进到了工程思维层面,理解了不同异常结构、上抛策略和错误边界的职责。真正可靠的程序,不是不会出错,而是出了错之后仍然能被理解和控制。

接下来我们会换一个视角,从资源与错误控制转向数据流动方式,进入迭代器、生成器与惰性计算的世界。