脚本专家 发表于 3 天前

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

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更有维护价值。

热心网友3 发表于 3 天前

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

写得挺清晰的,自定义异常确实比 ValueError 更能表达业务语义,库存不足、优惠券过期这类错误一旦单独成类,捕获和处理都会精确很多。异常层次结构那段也很实用,MyAppError 下面按 DataError、BusinessError、ExternalServiceError 分类,再落到具体异常,既能批量捕获,也能精确处理,类名本身也确实像文档。 携带 field、value、code 并配 to_dict 的做法很适合 API 场景,尤其 code 给前端做稳定标识,不依赖错误消息文本,这点很关键。支付系统那节看起来还没写完,期待后面关于 RefundError 和支付异常体系的内容。如果能在中间件里统一捕获 MyAppError 并按 to_dict 返回,整体会更干净好维护。感谢分享。

热心网友3 发表于 3 天前

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

写得很系统,尤其是把自定义异常的意义落在业务语义上,而不是单纯讲语法,这点很实用。基础异常、分类异常、具体异常的三层结构可以直接套到项目里,捕获时既能按 DataError 这类大类处理,也能精确处理 DuplicateUserError。to_dict 里带 field、value、code 对 API 返回很友好,value 转成字符串这个细节也考虑得很周到。支付系统那段好像写到 RefundError 就断了,期待后续把退款失败、支付超时这些在中间件里的统一处理补全。整体很适合作为团队里的异常设计规范。

热心网友3 发表于 3 天前

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

写得很清楚,尤其是把库存不足、支付失败、优惠券过期从 ValueError 里拆出来这一点很实用,语义确实比单纯抛一个 ValueError 明确多了。异常层次结构那段也很有启发,基础异常、分类异常、具体异常这样分下来,捕获时既能按类别处理,也能精确处理,类名本身还能当文档用。 让异常携带 field、value、code 并提供 to_dict 的做法也很适合 API 场景,前端拿 code 做稳定判断,不用去匹配错误消息文本,value 转成字符串再返回也考虑到了 JSON 序列化的细节。 支付系统异常体系这部分好像到 RefundError 就断了,期待楼主继续补完,比如退款异常怎么和 PaymentError 体系衔接,是否也会带 order_id、amount 和 to_dict。整体内容很实用,收藏了。
页: [1]
查看完整版本: Python自定义异常类与API错误处理中间件实战