Python 自定义异常在工程中的设计规范

4次阅读

工程中不能直接 raise Exception(‘xxx’),因其导致错误类型无法区分、日志监控失效、单元测试难断言;应继承具体异常类并按领域语义命名,集中定义、分层设计(领域/基础设施/封装异常),构造函数需保留上下文,__str__ 避免敏感信息,对预期业务分支建议用 Result 类型而非异常。

Python 自定义异常在工程中的设计规范

为什么不能直接 raise Exception(‘xxx’)

工程中直接 raise Exception('xxx') 会导致调用方无法区分错误类型,所有异常都落在同一个基类上,没法做针对性处理。比如数据库连接失败、参数校验不通过、第三方服务超时,这三类问题本应触发不同恢复逻辑,但全被 Exception 吞掉后,只能统一打印日志或重启服务。

更实际的问题是:日志监控系统(如 sentryelk)靠异常类名做聚合告警,如果全是 Exception,就失去分类统计意义;单元测试里也很难精准 assertRaises 某个业务场景。

  • 必须继承 Exception 或其子类(如 ValueErrorRuntimeError),不能裸用基类
  • 异常类名要体现领域语义,例如 UserNotFoundErrorPaymentValidationFailed,而不是 MyException
  • 模块内异常建议集中定义在 exceptions.py,避免散落在各处导致重复或遗漏

如何设计分层的异常继承体系

大型项目需要按错误来源和处理责任分层,不是所有异常都要平级定义。常见三层结构:

  • 领域异常(Domain Exceptions):如 InsufficientBalanceError,表示业务规则被违反,调用方应检查输入或引导用户操作
  • 基础设施异常(Infrastructure Exceptions):如 DatabaseConnectionErrorredisTimeoutError,表示外部依赖不可用,通常需重试或降级
  • 封装异常(Wrapper Exceptions):如 UserServiceError,用于对外暴露统一入口异常,屏蔽内部实现细节

注意:不要为了分层而深继承。例如 class DatabaseConnectionError(InfraError)class InfraError(ServiceError)class ServiceError(Exception) 这种四层链容易让开发者困惑“到底该 catch 哪一层”。一般两层足够:BaseappError + 具体业务/ infra 异常。

立即学习Python免费学习笔记(深入)”;

__init__ 和 __str__ 怎么写才利于调试

自定义异常的构造函数别只传 message,要保留原始上下文。尤其当异常是包装底层异常(如把 psycopg2.OperationalError 转成自己的 DatabaseError)时,必须用 cause 参数或 __cause__ 显式关联,否则 traceback 会丢失根因。

class DatabaseError(Exception):     def __init__(self, operation: str, detail: str = "", original_error: Exception = None):         self.operation = operation         self.detail = detail         self.original_error = original_error         message = f"DB {operation} failed: {detail}"         super().__init__(message)      def __str__(self):         s = super().__str__()         if self.original_error:             s += f" (caused by {type(self.original_error).__name__})"         return s
  • 避免在 __str__ 里拼接敏感数据(如密码、Token),生产环境可能被日志明文记录
  • 如果异常带字段(如 self.user_id),确保这些字段能被结构化日志采集器(如 structlog)自动提取
  • 不要重载 __repr__,除非有明确序列化需求;默认行为已足够调试

什么时候该用异常,什么时候该返回 Result 类型

python 没有内置 Result 类型,但像 result 库或手写的 Ok/Error 元组,在某些场景比抛异常更合适——尤其是错误是正常业务流一部分时。例如用户登录时邮箱格式错误,这不是“异常情况”,而是预期中的输入校验分支。

  • 用异常:表示「程序无法继续执行」或「契约被破坏」,如文件不存在却必须读取、http 500 响应、数据库事务中断
  • 用返回值:表示「有多种合法结果」,如搜索接口查无结果(NoneResult.empty())、API 返回 404(可转为 NotFound 对象而非抛异常)
  • 混用风险:同一模块里既抛 UserNotFoundError 又返回 Optional[User],会让调用方无所适从。建议按模块职责约定:DAO 层可用异常,Service 层倾向返回值,API 层再根据 HTTP 状态码决定是否转为异常

真正难处理的不是定义异常,而是团队对“什么算错误”的认知是否一致。一个 OrderAlreadyPaidError 在支付网关模块是 fatal,在订单查询模块可能只是返回 False —— 这种差异必须写进接口文档,而不是靠异常名猜。

text=ZqhQzanResources