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

为什么不能直接 raise Exception('xxx')
工程中直接 raise Exception('xxx') 会导致调用方无法区分错误类型,所有异常都落在同一个基类上,没法做针对性处理。比如数据库连接失败、参数校验不通过、第三方服务超时,这三类问题本应触发不同恢复逻辑,但全被 Exception 吞掉后,只能统一打印日志或重启服务。
更实际的问题是:日志监控系统(如 Sentry、ELK)靠异常类名做聚合告警,如果全是 Exception,就失去分类统计意义;单元测试里也很难精准 assertRaises 某个业务场景。
- 必须继承
Exception或其子类(如ValueError、RuntimeError),不能裸用基类 - 异常类名要体现领域语义,例如
UserNotFoundError、PaymentValidationFailed,而不是MyException - 模块内异常建议集中定义在
exceptions.py,避免散落在各处导致重复或遗漏
如何设计分层的异常继承体系
大型项目需要按错误来源和处理责任分层,不是所有异常都要平级定义。常见三层结构:
-
领域异常(Domain Exceptions):如
InsufficientBalanceError,表示业务规则被违反,调用方应检查输入或引导用户操作 -
基础设施异常(Infrastructure Exceptions):如
DatabaseConnectionError、RedisTimeoutError,表示外部依赖不可用,通常需重试或降级 -
封装异常(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 响应、数据库事务中断
- 用返回值:表示「有多种合法结果」,如搜索接口查无结果(
None或Result.empty())、API 返回 404(可转为NotFound对象而非抛异常) - 混用风险:同一模块里既抛
UserNotFoundError又返回Optional[User],会让调用方无所适从。建议按模块职责约定:DAO 层可用异常,Service 层倾向返回值,API 层再根据 HTTP 状态码决定是否转为异常
真正难处理的不是定义异常,而是团队对“什么算错误”的认知是否一致。一个 OrderAlreadyPaidError 在支付网关模块是 fatal,在订单查询模块可能只是返回 False —— 这种差异必须写进接口文档,而不是靠异常名猜。










