查看: 220|回复: 0

Python自定义异常类与API错误处理中间件实战

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
Python内置异常覆盖了大多数错误场景,但在电商、支付、API等应用中,ValueError往往说不清到底是库存不足、优惠券过期还是参数非法。自定义异常的本质就是继承Exception的普通类,用来表达特定于应用的错误。

一、创建自定义异常类

先看最小用法:为库存不足、支付失败、优惠券过期分别定义异常类。它们都继承Exception,命名以Error或Exception结尾。抛出时携带业务信息,捕获时就能精确处理。
  1. class InsufficientStockError(Exception):
  2.     '''库存不足异常'''
  3.     pass
  4. class PaymentFailedError(Exception):
  5.     '''支付失败异常'''
  6.     pass
  7. class CouponExpiredError(Exception):
  8.     '''优惠券过期异常'''
  9.     pass
  10. def place_order(product_id, quantity, stock):
  11.     if quantity > stock:
  12.         raise InsufficientStockError(
  13.             f'库存不足: 需要{quantity}件,实际库存{stock}件'
  14.         )
  15. try:
  16.     place_order('P001', 10, 5)
  17. except InsufficientStockError as e:
  18.     print(f'下单失败: {e}')
复制代码

这里的关键不是语法,而是语义:内置ValueError无法区分库存不足和价格非法,而InsufficientStockError可以。

二、设计异常层次结构

当项目规模变大时,不要散落定义异常。更合适的做法是建立层次结构:基础异常 -> 分类异常 -> 具体异常。基础异常MyAppError统一继承Exception;DataError、BusinessError、ExternalServiceError按错误来源分类;具体异常继承对应分类。
  1. class MyAppError(Exception):
  2.     '''应用的基础异常类'''
  3.     pass
  4. class DataError(MyAppError):
  5.     '''数据相关错误'''
  6.     pass
  7. class BusinessError(MyAppError):
  8.     '''业务逻辑错误'''
  9.     pass
  10. class ExternalServiceError(MyAppError):
  11.     '''外部服务错误'''
  12.     pass
  13. class ValidationError(DataError):
  14.     '''数据验证失败'''
  15.     pass
  16. class DuplicateUserError(DataError):
  17.     '''用户已存在'''
  18.     pass
  19. class InsufficientBalanceError(BusinessError):
  20.     '''余额不足'''
  21.     pass
  22. class PaymentTimeoutError(ExternalServiceError):
  23.     '''支付超时'''
  24.     pass
复制代码

这种结构有三个直接好处:可以按类别捕获,比如except DataError捕获所有数据错误;可以精确捕获,比如只处理DuplicateUserError;异常类名本身就是代码文档,阅读调用链时能看出可能出什么错。

三、让异常携带上下文

如果异常只带一条消息,API和日志很难定位问题。可以在异常类的__init__中接收field、value、code等参数,并提供一个to_dict方法,方便Web API返回结构化错误。
  1. class ValidationError(Exception):
  2.     '''数据验证错误——携带详细的字段信息'''
  3.     def __init__(self, message, field=None, value=None, code=None):
  4.         super().__init__(message)
  5.         self.field = field
  6.         self.value = value
  7.         self.code = code
  8.     def to_dict(self):
  9.         '''转为字典——方便API返回'''
  10.         return {
  11.             'error': str(self),
  12.             'field': self.field,
  13.             'value': str(self.value) if self.value else None,
  14.             'code': self.code or 'VALIDATION_ERROR',
  15.         }
  16. def validate_product(data):
  17.     if not data.get('name'):
  18.         raise ValidationError(
  19.             '产品名称不能为空',
  20.             field='name',
  21.             code='MISSING_NAME'
  22.         )
  23.     if data.get('price', 0) <= 0:
  24.         raise ValidationError(
  25.             '产品价格必须大于0',
  26.             field='price',
  27.             value=data.get('price'),
  28.             code='INVALID_PRICE'
  29.         )
  30. try:
  31.     validate_product({'name': '产品A', 'price': -10})
  32. except ValidationError as e:
  33.     print(e.to_dict())
复制代码

输出会是:
  1. {'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保留原因链。
  1. class PaymentError(Exception):
  2.     '''支付相关错误的基类'''
  3.     def __init__(self, message, order_id=None, amount=None):
  4.         super().__init__(message)
  5.         self.order_id = order_id
  6.         self.amount = amount
  7. class PaymentDeclinedError(PaymentError):
  8.     '''支付被拒绝'''
  9.     def __init__(self, message, order_id=None, amount=None, reason=None):
  10.         super().__init__(message, order_id, amount)
  11.         self.reason = reason
  12. class PaymentTimeoutError(PaymentError):
  13.     '''支付超时'''
  14.     def __init__(self, message, order_id=None, amount=None, timeout_seconds=None):
  15.         super().__init__(message, order_id, amount)
  16.         self.timeout_seconds = timeout_seconds
  17. class RefundError(PaymentError):
  18.     '''退款失败'''
  19.     def __init__(self, message, order_id=None, amount=None, refund_id=None):
  20.         super().__init__(message, order_id, amount)
  21.         self.refund_id = refund_id
  22. class PaymentService:
  23.     @staticmethod
  24.     def pay(order_id, amount, payment_method):
  25.         '''执行支付'''
  26.         try:
  27.             response = PaymentService._call_gateway(order_id, amount, payment_method)
  28.             if response['status'] == 'declined':
  29.                 reason_msg = response.get('message', '未知原因')
  30.                 raise PaymentDeclinedError(
  31.                     f'支付被拒绝: {reason_msg}',
  32.                     order_id=order_id,
  33.                     amount=amount,
  34.                     reason=response.get('reason')
  35.                 )
  36.             return response
  37.         except TimeoutError as e:
  38.             raise PaymentTimeoutError(
  39.                 '支付网关超时',
  40.                 order_id=order_id,
  41.                 amount=amount,
  42.                 timeout_seconds=30
  43.             ) from e
  44.     @staticmethod
  45.     def _call_gateway(order_id, amount, method):
  46.         return {'status': 'declined', 'reason': 'insufficient_funds'}
  47. service = PaymentService()
  48. try:
  49.     result = service.pay('ORD-001', 299.99, 'credit_card')
  50. except PaymentDeclinedError as e:
  51.     print(f'支付失败: {e}')
  52.     print(f' 订单: {e.order_id}')
  53.     print(f' 金额: {e.amount}')
  54.     print(f' 原因: {e.reason}')
  55. except PaymentTimeoutError as e:
  56.     print(f'支付超时: {e}')
  57. except PaymentError as e:
  58.     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,避免把内部堆栈暴露给客户端。
  1. class APIError(Exception):
  2.     '''API错误的基类'''
  3.     status_code = 500
  4.     error_code = 'INTERNAL_ERROR'
  5.     def __init__(self, message, details=None):
  6.         super().__init__(message)
  7.         self.details = details or {}
  8. class BadRequestError(APIError):
  9.     status_code = 400
  10.     error_code = 'BAD_REQUEST'
  11. class NotFoundError(APIError):
  12.     status_code = 404
  13.     error_code = 'NOT_FOUND'
  14. class UnauthorizedError(APIError):
  15.     status_code = 401
  16.     error_code = 'UNAUTHORIZED'
  17. class ForbiddenError(APIError):
  18.     status_code = 403
  19.     error_code = 'FORBIDDEN'
  20. def handle_api_error(func):
  21.     '''装饰器——统一处理API异常'''
  22.     def wrapper(*args, **kwargs):
  23.         try:
  24.             return func(*args, **kwargs)
  25.         except APIError as e:
  26.             return {
  27.                 'success': False,
  28.                 'error': {
  29.                     'code': e.error_code,
  30.                     'message': str(e),
  31.                     'details': e.details,
  32.                 }
  33.             }, e.status_code
  34.         except Exception as e:
  35.             import logging
  36.             logging.exception('未预期的服务器错误')
  37.             return {
  38.                 'success': False,
  39.                 'error': {
  40.                     'code': 'INTERNAL_ERROR',
  41.                     'message': '服务器内部错误',
  42.                 }
  43.             }, 500
  44.     return wrapper
  45. @handle_api_error
  46. def get_user(user_id):
  47.     '''获取用户——API处理函数'''
  48.     if not isinstance(user_id, int) or user_id <= 0:
  49.         raise BadRequestError(
  50.             '无效的用户ID',
  51.             details={'user_id': user_id}
  52.         )
  53.     user = find_user(user_id)
  54.     if user is None:
  55.         raise NotFoundError(
  56.             f'用户{user_id}不存在',
  57.             details={'user_id': user_id}
  58.         )
  59.     return {'success': True, 'data': user}
  60. def find_user(user_id):
  61.     return None
  62. result, status = get_user(999)
  63. print(f'状态码: {status}')
  64. 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更有维护价值。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-11 16:54 , Processed in 0.030572 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部