Python内置异常覆盖了大多数错误场景,但在电商、支付、API等应用中,ValueError往往说不清到底是库存不足、优惠券过期还是参数非法。自定义异常的本质就是继承Exception的普通类,用来表达特定于应用的错误。
一、创建自定义异常类
先看最小用法:为库存不足、支付失败、优惠券过期分别定义异常类。它们都继承Exception,命名以Error或Exception结尾。抛出时携带业务信息,捕获时就能精确处理。
- class InsufficientStockError(Exception):
- '''库存不足异常'''
- pass
- class PaymentFailedError(Exception):
- '''支付失败异常'''
- pass
- class CouponExpiredError(Exception):
- '''优惠券过期异常'''
- pass
- def place_order(product_id, quantity, stock):
- if quantity > stock:
- raise InsufficientStockError(
- f'库存不足: 需要{quantity}件,实际库存{stock}件'
- )
- try:
- place_order('P001', 10, 5)
- except InsufficientStockError as e:
- print(f'下单失败: {e}')
复制代码
这里的关键不是语法,而是语义:内置ValueError无法区分库存不足和价格非法,而InsufficientStockError可以。
二、设计异常层次结构
当项目规模变大时,不要散落定义异常。更合适的做法是建立层次结构:基础异常 -> 分类异常 -> 具体异常。基础异常MyAppError统一继承Exception;DataError、BusinessError、ExternalServiceError按错误来源分类;具体异常继承对应分类。
- class MyAppError(Exception):
- '''应用的基础异常类'''
- pass
- class DataError(MyAppError):
- '''数据相关错误'''
- pass
- class BusinessError(MyAppError):
- '''业务逻辑错误'''
- pass
- class ExternalServiceError(MyAppError):
- '''外部服务错误'''
- pass
- class ValidationError(DataError):
- '''数据验证失败'''
- pass
- class DuplicateUserError(DataError):
- '''用户已存在'''
- pass
- class InsufficientBalanceError(BusinessError):
- '''余额不足'''
- pass
- class PaymentTimeoutError(ExternalServiceError):
- '''支付超时'''
- pass
复制代码
这种结构有三个直接好处:可以按类别捕获,比如except DataError捕获所有数据错误;可以精确捕获,比如只处理DuplicateUserError;异常类名本身就是代码文档,阅读调用链时能看出可能出什么错。
三、让异常携带上下文
如果异常只带一条消息,API和日志很难定位问题。可以在异常类的__init__中接收field、value、code等参数,并提供一个to_dict方法,方便Web API返回结构化错误。
- class ValidationError(Exception):
- '''数据验证错误——携带详细的字段信息'''
- def __init__(self, message, field=None, value=None, code=None):
- super().__init__(message)
- self.field = field
- self.value = value
- self.code = code
- def to_dict(self):
- '''转为字典——方便API返回'''
- return {
- 'error': str(self),
- 'field': self.field,
- 'value': str(self.value) if self.value else None,
- 'code': self.code or 'VALIDATION_ERROR',
- }
- def validate_product(data):
- if not data.get('name'):
- raise ValidationError(
- '产品名称不能为空',
- field='name',
- code='MISSING_NAME'
- )
- if data.get('price', 0) <= 0:
- raise ValidationError(
- '产品价格必须大于0',
- field='price',
- value=data.get('price'),
- code='INVALID_PRICE'
- )
- try:
- validate_product({'name': '产品A', 'price': -10})
- except ValidationError as e:
- print(e.to_dict())
复制代码
输出会是:- {'error': '产品价格必须大于0', 'field': 'price', 'value': '-10', 'code': 'INVALID_PRICE'}
复制代码
注意,value在to_dict中转成了字符串,这样能安全放入JSON响应。code用于给前端或调用方提供稳定标识,不依赖错误消息文本。
四、支付系统中的异常体系
支付场景是自定义异常的典型场景。可以定义PaymentError作为基类,携带order_id和amount;PaymentDeclinedError增加reason;PaymentTimeoutError增加timeout_seconds;RefundError增加refund_id。支付服务调用网关时,根据响应状态抛出不同异常,超时则把TimeoutError转换成PaymentTimeoutError,并用from e保留原因链。
- class PaymentError(Exception):
- '''支付相关错误的基类'''
- def __init__(self, message, order_id=None, amount=None):
- super().__init__(message)
- self.order_id = order_id
- self.amount = amount
- class PaymentDeclinedError(PaymentError):
- '''支付被拒绝'''
- def __init__(self, message, order_id=None, amount=None, reason=None):
- super().__init__(message, order_id, amount)
- self.reason = reason
- class PaymentTimeoutError(PaymentError):
- '''支付超时'''
- def __init__(self, message, order_id=None, amount=None, timeout_seconds=None):
- super().__init__(message, order_id, amount)
- self.timeout_seconds = timeout_seconds
- class RefundError(PaymentError):
- '''退款失败'''
- def __init__(self, message, order_id=None, amount=None, refund_id=None):
- super().__init__(message, order_id, amount)
- self.refund_id = refund_id
- class PaymentService:
- @staticmethod
- def pay(order_id, amount, payment_method):
- '''执行支付'''
- try:
- response = PaymentService._call_gateway(order_id, amount, payment_method)
- if response['status'] == 'declined':
- reason_msg = response.get('message', '未知原因')
- raise PaymentDeclinedError(
- f'支付被拒绝: {reason_msg}',
- order_id=order_id,
- amount=amount,
- reason=response.get('reason')
- )
- return response
- except TimeoutError as e:
- raise PaymentTimeoutError(
- '支付网关超时',
- order_id=order_id,
- amount=amount,
- timeout_seconds=30
- ) from e
- @staticmethod
- def _call_gateway(order_id, amount, method):
- return {'status': 'declined', 'reason': 'insufficient_funds'}
- service = PaymentService()
- try:
- result = service.pay('ORD-001', 299.99, 'credit_card')
- except PaymentDeclinedError as e:
- print(f'支付失败: {e}')
- print(f' 订单: {e.order_id}')
- print(f' 金额: {e.amount}')
- print(f' 原因: {e.reason}')
- except PaymentTimeoutError as e:
- print(f'支付超时: {e}')
- except PaymentError as e:
- print(f'其他支付错误: {e}')
复制代码
五、API异常处理中间件
Web API中常需要把异常统一转换成HTTP状态码和JSON结构。可以定义APIError基类,类属性status_code和error_code;子类BadRequestError、NotFoundError、UnauthorizedError、ForbiddenError分别设置400、404、401、403。再用装饰器handle_api_error包裹处理函数:遇到APIError时返回对应状态码和错误详情;遇到未预期异常时记录日志并返回500,避免把内部堆栈暴露给客户端。
- class APIError(Exception):
- '''API错误的基类'''
- status_code = 500
- error_code = 'INTERNAL_ERROR'
- def __init__(self, message, details=None):
- super().__init__(message)
- self.details = details or {}
- class BadRequestError(APIError):
- status_code = 400
- error_code = 'BAD_REQUEST'
- class NotFoundError(APIError):
- status_code = 404
- error_code = 'NOT_FOUND'
- class UnauthorizedError(APIError):
- status_code = 401
- error_code = 'UNAUTHORIZED'
- class ForbiddenError(APIError):
- status_code = 403
- error_code = 'FORBIDDEN'
- def handle_api_error(func):
- '''装饰器——统一处理API异常'''
- def wrapper(*args, **kwargs):
- try:
- return func(*args, **kwargs)
- except APIError as e:
- return {
- 'success': False,
- 'error': {
- 'code': e.error_code,
- 'message': str(e),
- 'details': e.details,
- }
- }, e.status_code
- except Exception as e:
- import logging
- logging.exception('未预期的服务器错误')
- return {
- 'success': False,
- 'error': {
- 'code': 'INTERNAL_ERROR',
- 'message': '服务器内部错误',
- }
- }, 500
- return wrapper
- @handle_api_error
- def get_user(user_id):
- '''获取用户——API处理函数'''
- if not isinstance(user_id, int) or user_id <= 0:
- raise BadRequestError(
- '无效的用户ID',
- details={'user_id': user_id}
- )
- user = find_user(user_id)
- if user is None:
- raise NotFoundError(
- f'用户{user_id}不存在',
- details={'user_id': user_id}
- )
- return {'success': True, 'data': user}
- def find_user(user_id):
- return None
- result, status = get_user(999)
- print(f'状态码: {status}')
- print(f'响应: {result}')
复制代码
调用get_user(999)时,find_user返回None,因此抛出NotFoundError,最终返回状态码404,响应中success为False,error.code为NOT_FOUND,details里带有user_id。这个模式适合在Flask、Django等框架之外先理解异常转换逻辑,再接入框架自己的错误处理器。
六、要点与适用场景
自定义异常的核心价值,是把“通用错误”升级为“领域专用错误”。实现时注意几点:继承Exception而不是BaseException,因为BaseException通常留给系统退出等信号;为项目建立基础异常->分类->具体异常的层次结构;异常类可以携带field、value、code、order_id、amount、timeout_seconds等上下文;命名以Error或Exception结尾。内置异常无法精确描述错误时,就应该创建自定义异常;好的异常命名本身就是文档。
适用场景包括电商下单、支付网关调用、API参数校验、用户查询、退款流程等。只要调用方需要根据错误类型走不同分支,或者需要向API返回稳定错误码,自定义异常体系就比单一ValueError更有维护价值。 |