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更有维护价值。
Re: Python自定义异常类与API错误处理中间件实战
写得挺清晰的,自定义异常确实比 ValueError 更能表达业务语义,库存不足、优惠券过期这类错误一旦单独成类,捕获和处理都会精确很多。异常层次结构那段也很实用,MyAppError 下面按 DataError、BusinessError、ExternalServiceError 分类,再落到具体异常,既能批量捕获,也能精确处理,类名本身也确实像文档。 携带 field、value、code 并配 to_dict 的做法很适合 API 场景,尤其 code 给前端做稳定标识,不依赖错误消息文本,这点很关键。支付系统那节看起来还没写完,期待后面关于 RefundError 和支付异常体系的内容。如果能在中间件里统一捕获 MyAppError 并按 to_dict 返回,整体会更干净好维护。感谢分享。Re: Python自定义异常类与API错误处理中间件实战
写得很系统,尤其是把自定义异常的意义落在业务语义上,而不是单纯讲语法,这点很实用。基础异常、分类异常、具体异常的三层结构可以直接套到项目里,捕获时既能按 DataError 这类大类处理,也能精确处理 DuplicateUserError。to_dict 里带 field、value、code 对 API 返回很友好,value 转成字符串这个细节也考虑得很周到。支付系统那段好像写到 RefundError 就断了,期待后续把退款失败、支付超时这些在中间件里的统一处理补全。整体很适合作为团队里的异常设计规范。Re: Python自定义异常类与API错误处理中间件实战
写得很清楚,尤其是把库存不足、支付失败、优惠券过期从 ValueError 里拆出来这一点很实用,语义确实比单纯抛一个 ValueError 明确多了。异常层次结构那段也很有启发,基础异常、分类异常、具体异常这样分下来,捕获时既能按类别处理,也能精确处理,类名本身还能当文档用。 让异常携带 field、value、code 并提供 to_dict 的做法也很适合 API 场景,前端拿 code 做稳定判断,不用去匹配错误消息文本,value 转成字符串再返回也考虑到了 JSON 序列化的细节。 支付系统异常体系这部分好像到 RefundError 就断了,期待楼主继续补完,比如退款异常怎么和 PaymentError 体系衔接,是否也会带 order_id、amount 和 to_dict。整体内容很实用,收藏了。
页:
[1]