当前位置: 首页 > news >正文

Python异常处理实战:从语法错误到生产级容错

1. 项目概述:为什么你写的Python程序总在半夜报警,而别人的却能安静跑完一整年?

我带过十几支数据工程和后端开发团队,最常听到的吐槽不是“功能做不出来”,而是“线上服务又崩了,但日志里只有一行红色报错,根本看不出哪行代码出的问题”。有一次凌晨三点被电话叫醒,排查了两小时才发现是某个上游API返回了空JSON,而我们的代码直接json.loads(response.text)——没加任何校验。这种问题不难解决,但代价是团队所有人的睡眠和用户信任。Python的异常处理机制不是锦上添花的语法糖,而是生产环境的呼吸阀。它决定了你的程序是优雅降级、静默恢复,还是轰然倒塌、全链路雪崩。这篇文章不讲教科书定义,只讲我在真实项目里踩过的坑、验证过的方案、以及写进SOP的硬核操作。你会看到:为什么except:except Exception:更危险;为什么finally里关文件可能比不关更致命;为什么Python 3.11的add_note()让故障复盘时间从2小时缩短到15分钟;还有那些官方文档绝不会告诉你的、关于__cause____context__的幽灵陷阱。如果你写过try...except但依然被线上告警追着跑,或者刚学完基础语法却不敢碰生产代码——这篇就是为你写的。它不假设你懂装饰器或协程,但要求你愿意把print("debug")换成真正的错误诊断逻辑。

2. 错误与异常的本质区别:别再用“语法错误”糊弄自己了

2.1 编译期错误 vs 运行时异常:Python的两道生死线

很多初学者把SyntaxErrorZeroDivisionError都叫“报错”,这就像把“心脏骤停”和“感冒发烧”都叫“不舒服”。Python的错误体系有明确的分水岭:编译期错误(Errors)和运行时异常(Exceptions)。前者发生在代码执行前,后者发生在执行中。这个区别直接决定你能否“救活”程序。

SyntaxError是Python解释器在将源码编译成字节码时发现的结构性问题。比如少了个冒号、括号没配对、return写在函数外——这些错误连python -c "print(1)"都通不过。关键在于:这类错误根本不会生成.pyc文件,程序连启动的机会都没有。我见过最典型的案例是某金融团队把if x > 0: pass写成if x > 0 pass(漏了冒号),CI流水线卡在构建阶段,但开发人员坚持说“本地能跑”,因为他在Jupyter里用%run命令执行了单行代码块,而Jupyter对语法检查更宽松。解决方案极其简单:在CI脚本里加一行python -m py_compile your_module.py,强制触发编译检查。这行命令会在编译失败时返回非零退出码,直接中断流水线。

ZeroDivisionError这类异常,是程序已经跑起来了,但在执行某条指令时遇到了无法处理的状况。它的核心特征是:可被捕获、可被干预、可被重试。比如除零错误,你完全可以捕获后返回默认值、记录告警、甚至调用备用算法。我在电商大促系统里就做过类似设计:当实时库存服务超时,主流程捕获TimeoutError后,自动切换到本地缓存库存,并发送企业微信告警“库存服务延迟,已启用降级策略”。这种能力让系统从“脆弱”变成“韧性”。

提示:用python -m py_compile检查语法错误是上线前的必做动作。不要依赖IDE的实时提示——有些语法错误(如f-string嵌套表达式中的括号匹配)只有在真正编译时才会暴露。

2.2 为什么“错误不该被捕获”是句废话?现实中的灰色地带

官方文档说“Errors不应被捕获”,但真实世界充满灰色地带。比如MemoryError:当你的数据处理脚本在分析10GB日志时内存耗尽,捕获它并优雅退出总比让整个服务器OOM Killer干掉所有进程强。再比如SystemExit:某些运维脚本需要在特定条件下主动退出,但若被上层except:吞掉,会导致僵尸进程堆积。我的做法是建立分层捕获策略:

  • 顶层兜底:只捕获Exception(排除SystemExit,KeyboardInterrupt等)
  • 中间层:针对业务场景捕获具体异常,如requests.exceptions.ConnectionError
  • 底层防御:在资源密集型操作前预检,如用psutil.virtual_memory().available判断剩余内存

这里有个血泪教训:某次我们用pandas.read_csv()读取超大文件,没加chunksize参数,导致内存爆满。捕获MemoryError后尝试释放内存,却发现Python的垃圾回收器在内存压力下反而变慢。最终方案是改用dask.dataframe流式处理,并在try块外加内存监控钩子。异常处理不是万能胶,而是手术刀——要精准切开问题,而不是糊住伤口。

2.3 内置异常的家族图谱:90%的线上问题都出自这7个类

Python内置异常有60+种,但实际项目中90%的故障集中在以下7个类。我把它们按“破坏力”和“可修复性”做了分级,这是我在12个生产系统中统计出的真实数据:

异常类型典型场景是否可预测推荐处理方式真实案例
ValueErrorint("abc"),datetime.strptime("2023", "%Y-%m-%d")输入校验+默认值支付系统解析用户输入的日期格式错误,导致订单状态机卡死
KeyError/IndexError字典取不存在的key、列表索引越界中高dict.get()/try-except+日志推荐引擎从Redis获取用户画像时key缺失,未设默认值导致推荐流中断
ConnectionErrorrequests请求超时、数据库连接断开重试机制+熔断第三方天气API不稳定,未加重试导致App首页天气模块空白
FileNotFoundError读取配置文件、加载模型权重文件失败启动时预检+备用路径模型服务启动时找不到model.pth,因Docker镜像构建遗漏文件
TypeErrorlen(123),str + int类型注解+pre-commit检查数据管道中字符串字段被误转为int,下游SQL查询报错
RuntimeError多线程竞争、异步事件循环关闭后调用根本原因修复FastAPI应用中全局事件循环被意外关闭,导致所有异步任务挂起
OSError磁盘满、权限不足、网络不可达资源监控+告警日志服务磁盘写满,logging.FileHandler抛出OSError,但未触发清理逻辑

注意OSError的特殊性:它是IOErrorFileNotFoundErrorPermissionError等的父类。在Linux系统中,OSError.errno会返回具体的POSIX错误码(如errno=28表示磁盘空间不足)。我在一个日志聚合服务中就利用这点做了精细化处理:当捕获到OSErrorerr.errno == 28时,自动触发日志轮转和旧日志删除,而不是简单报错。

3. try-except-else-finally 的深度实践:别再写“裸except”了

3.1 为什么except:是生产环境的定时炸弹?

新手最爱写except:,觉得“反正抓住所有错误就行”。但这是最危险的习惯。看这个真实案例:某支付网关的回调处理函数写了这样的代码:

def handle_callback(data): try: # 处理支付结果 process_payment(data) # 更新数据库 update_order_status(data['order_id']) # 发送消息队列 send_kafka_message(data) except: # 记录错误 logger.error("Callback failed") return False

表面看没问题,但当send_kafka_message()因网络问题抛出KafkaTimeoutError时,这个except:会把它和process_payment()里的ValueError同等对待。结果是:支付成功了,数据库更新了,但消息没发出去,导致下游风控系统收不到通知。更糟的是,except:还会捕获KeyboardInterrupt(Ctrl+C)和SystemExit,导致运维人员无法正常终止进程。

正确姿势是:永远指定异常类型,或至少用except Exception:为什么不是except BaseException:?因为BaseException包含SystemExitKeyboardInterrupt,捕获它们会让程序无法被正常终止。我在金融系统中严格规定:所有except必须显式列出类型,禁止裸except。CI流水线用pylint检查,违规直接拒绝合并。

3.2 else块的隐藏价值:分离“成功路径”与“错误处理”

很多人忽略else,觉得“不报错就执行,那直接写在try里不就行了?”错!else的核心价值是隔离成功逻辑,避免误捕异常。看这个反模式:

# 反模式:在try里混入成功逻辑 try: result = risky_operation() # 下面这行也可能抛异常! send_notification(result) # 可能因网络问题抛出ConnectionError except ValueError: handle_value_error()

这里send_notification()的异常会被except ValueError吞掉,导致问题难以定位。正确写法是:

# 正模式:用else明确成功路径 try: result = risky_operation() except ValueError as e: handle_value_error(e) else: # 只有risky_operation()成功才执行 send_notification(result) # 这里的异常不会被上面的except捕获

我在一个实时风控系统中大量使用此模式。risky_operation()是调用第三方反欺诈API,send_notification()是向Kafka推送决策结果。这样设计后,API调用失败走except分支记录审计日志,而Kafka推送失败则触发独立的告警通道,两者互不干扰。

3.3 finally的致命陷阱:你以为的“总会执行”,其实有条件

finally常被宣传为“无论如何都会执行”,但有两个例外:1)程序被os._exit()强制终止;2)发生SystemExitKeyboardInterrupt且未被捕获。更隐蔽的陷阱是:finally里的异常会覆盖try块中的异常。看这个经典例子:

def buggy_file_handler(): f = open("test.txt", "w") try: f.write("data") raise ValueError("Something went wrong") finally: f.close() # 如果这里出错,会掩盖ValueError!

如果f.close()因磁盘满抛出OSError,那么ValueError就永远消失了,你只会看到OSError。解决方案是:finally里做清理操作时,必须用独立的try-except包裹。我在所有文件操作中都采用这个模板:

def safe_file_handler(): f = None try: f = open("test.txt", "w") f.write("data") raise ValueError("Business logic error") except ValueError as e: logger.error(f"Business error: {e}") raise # 重新抛出,不掩盖 finally: if f is not None: try: f.close() except OSError as e: logger.warning(f"Failed to close file: {e}")

这个模式确保业务异常不被掩盖,同时清理失败也不会导致资源泄漏。

3.4 多重except的优先级规则:顺序决定命运

当多个except块存在时,Python按从上到下的顺序匹配,且子类异常必须放在父类之前。否则父类会提前捕获所有子类。看这个错误示范:

# 错误:父类在前,子类永远无法触发 try: raise ValueError("test") except Exception: # 这里会捕获所有异常,包括ValueError print("Caught Exception") except ValueError: # 这行永远不会执行 print("Caught ValueError")

正确顺序是:

try: raise ValueError("test") except ValueError: # 子类优先 print("Caught ValueError") except Exception: # 父类兜底 print("Caught other Exception")

我在一个微服务网关中应用此规则处理HTTP错误:先捕获requests.exceptions.Timeout,再捕获requests.exceptions.ConnectionError,最后用requests.exceptions.RequestException兜底。这样能针对不同网络问题执行差异化策略——超时可重试,连接拒绝需降级,其他异常则告警。

4. 高级异常处理实战:从防御到主动出击

4.1 自定义异常:让错误信息自带上下文

内置异常像通用扳手,自定义异常才是专用螺丝刀。关键原则:异常名要体现业务语义,构造函数要携带可诊断的上下文。看一个电商系统的例子:

class InsufficientStockError(Exception): """库存不足异常""" def __init__(self, sku_id: str, requested_qty: int, available_qty: int): self.sku_id = sku_id self.requested_qty = requested_qty self.available_qty = available_qty super().__init__( f"SKU {sku_id}: requested {requested_qty}, available {available_qty}" ) def to_dict(self) -> dict: """转换为结构化数据,便于日志和监控""" return { "error_type": "InsufficientStockError", "sku_id": self.sku_id, "requested_qty": self.requested_qty, "available_qty": self.available_qty } # 使用 def reserve_stock(sku_id: str, qty: int) -> bool: available = get_stock(sku_id) if available < qty: raise InsufficientStockError(sku_id, qty, available) # 执行扣减...

这样做的好处:1)日志中直接看到InsufficientStockError: SKU ABC123: requested 5, available 2;2)监控系统可基于error_type字段做聚合告警;3)前端可根据异常类型显示不同提示(如“库存不足”而非“系统错误”)。我在一个千万级用户平台中,通过自定义异常将客诉率降低了37%,因为客服能直接从错误码定位到具体SKU和库存缺口。

4.2 Python 3.11新特性实战:add_note()让调试效率翻倍

Python 3.11的add_note()是革命性的。以前遇到复杂异常,你得在except块里手动拼接上下文:

# Python 3.10及之前:手动拼接 try: process_user_data(user_id) except ValueError as e: # 把上下文硬塞进错误消息 new_msg = f"{e} | user_id={user_id} | timestamp={time.time()}" raise ValueError(new_msg) from e

现在用add_note()

# Python 3.11:干净利落 try: process_user_data(user_id) except ValueError as e: e.add_note(f"user_id={user_id}") e.add_note(f"request_id={request_id}") e.add_note(f"trace_id={trace_id}") raise # 重新抛出,note会保留在traceback中

效果是:traceback末尾自动追加Notes部分:

ValueError: Invalid user data Notes: - user_id=U123456 - request_id=R789012 - trace_id=T345678

我在一个分布式追踪系统中实测:故障定位时间从平均47分钟缩短到12分钟。因为运维不再需要翻查多份日志关联user_id,Notes里直接给出所有关键线索。注意:add_note()只能添加字符串,且不能修改原始异常消息,这是刻意设计的安全特性。

4.3 结构化异常处理:Python 3.10的match语句

Python 3.10的match语句让多异常处理告别“意大利面条代码”。传统写法:

# 传统方式:嵌套if-elif try: result = call_external_api() except Exception as e: if isinstance(e, requests.exceptions.Timeout): handle_timeout() elif isinstance(e, requests.exceptions.ConnectionError): handle_connection_error() elif isinstance(e, ValueError): handle_validation_error() else: handle_unknown_error()

match重构:

# match方式:声明式处理 try: result = call_external_api() except Exception as e: match e: case requests.exceptions.Timeout(): handle_timeout() case requests.exceptions.ConnectionError(): handle_connection_error() case ValueError(): handle_validation_error() case _: handle_unknown_error()

更强大的是模式匹配能力。比如处理API返回的不同错误码:

# 匹配异常属性 case requests.exceptions.HTTPError() as err if err.response.status_code == 401: refresh_auth_token() case requests.exceptions.HTTPError() as err if 400 <= err.response.status_code < 500: log_client_error(err) case requests.exceptions.HTTPError() as err if err.response.status_code >= 500: trigger_alert("Server error", err.response.status_code)

我在一个API网关项目中用此特性,将错误处理代码行数减少了62%,且可读性大幅提升——新成员一眼就能看出每种HTTP状态码对应的动作。

4.4 异常组(ExceptionGroup):并发世界的终极武器

Python 3.11的ExceptionGroup解决了异步/并发编程的老大难问题。以前asyncio.gather()遇到多个任务失败,只会抛出第一个异常,其余的都丢失了。现在:

import asyncio async def fetch_data(url): # 模拟可能失败的异步请求 if "error" in url: raise ValueError(f"Failed to fetch {url}") return f"Data from {url}" async def main(): urls = ["https://api1.com", "https://api2.com/error", "https://api3.com/error"] try: results = await asyncio.gather( *[fetch_data(url) for url in urls], return_exceptions=True # 关键:返回异常而非抛出 ) # 手动检查results中的异常 errors = [r for r in results if isinstance(r, Exception)] if errors: raise ExceptionGroup("Fetch failed", errors) except ExceptionGroup as eg: # 分别处理不同类型的异常 for exc in eg.exceptions: if isinstance(exc, ValueError): logger.warning(f"ValueError: {exc}") else: logger.error(f"Unexpected error: {exc}") # 或者用except*语法糖 try: await main() except* ValueError as eg: logger.warning(f"ValueErrors: {eg.exceptions}") except* Exception as eg: logger.error(f"Other errors: {eg.exceptions}")

我在一个实时数据同步服务中应用此特性:当同时拉取10个数据源时,即使3个失败,也能准确知道是哪3个、失败原因是什么,从而触发针对性的重试策略,而不是整个任务回滚。

5. 生产级异常处理SOP:从代码到监控的完整闭环

5.1 异常处理黄金法则:五不原则

基于十年运维经验,我总结出异常处理的“五不原则”,已在多个团队落地为编码规范:

  1. 不裸捕:禁用except:,必须指定异常类型或except Exception:
  2. 不静默:捕获异常后必须记录日志(至少ERROR级别),禁止except: pass
  3. 不掩盖finally中清理操作必须用独立try-except,避免覆盖主异常
  4. 不泛化:自定义异常必须继承Exception(非BaseException),避免捕获系统退出信号
  5. 不独断:对可恢复异常(如网络超时)必须提供重试或降级,而非直接失败

每个原则都有对应的pre-commit钩子检查。例如检测裸except的正则表达式:^\s*except\s*:$,CI流水线中失败即阻断。

5.2 日志与监控的协同设计:让异常自己说话

异常处理的价值最终体现在可观测性上。我的标准实践是:每个except块必须生成结构化日志,并关联监控指标。以数据库操作为例:

import logging from opentelemetry import metrics logger = logging.getLogger(__name__) meter = metrics.get_meter(__name__) db_errors_counter = meter.create_counter( "db.errors", description="Database operation errors" ) def execute_db_query(query: str): try: result = db.execute(query) return result except psycopg2.OperationalError as e: # 结构化日志:包含SQL、错误码、持续时间 logger.error( "DB OperationalError", extra={ "sql": query[:100], # 截断长SQL "pgcode": getattr(e, "pgcode", "N/A"), "duration_ms": time.time() - start_time } ) # 上报监控指标 db_errors_counter.add(1, {"type": "OperationalError", "pgcode": e.pgcode}) raise except Exception as e: logger.exception("Unexpected DB error") # 自动记录traceback db_errors_counter.add(1, {"type": "Unknown"}) raise

这样,当pgcode=08006(连接失败)时,监控大盘会立即亮起,告警规则可设置为“5分钟内同pgcode错误>3次”触发企业微信通知。而日志中的sql字段支持Elasticsearch全文检索,运维人员输入pgcode:08006就能秒级定位所有相关SQL。

5.3 故障复盘清单:每次线上事故后的必做动作

异常处理的终极目标不是“不出错”,而是“出错后快速恢复”。我要求团队每次P1级故障后填写《异常处理复盘清单》,包含:

  • 异常溯源:错误类型、发生位置(文件:行号)、调用栈关键帧
  • 处理路径:是否被捕获?哪个except块处理?处理逻辑是否合理?
  • 日志质量:日志中是否包含足够上下文(如用户ID、请求ID、关键变量值)?
  • 监控覆盖:该异常类型是否有对应监控指标?告警阈值是否合理?
  • 改进措施:增加输入校验?添加重试?优化SQL?还是根本性重构?

这份清单不是问责工具,而是知识沉淀。我们用它驱动自动化:当某类ValueError重复出现3次,CI流水线自动创建Issue并@相关开发者。过去一年,此类自动化拦截了73%的重复故障。

5.4 单元测试中的异常验证:用pytest写出可靠的防御代码

异常处理代码必须被测试覆盖。pytest提供了优雅的验证方式:

import pytest def test_insufficient_stock_raises_exception(): """测试库存不足时抛出自定义异常""" with pytest.raises(InsufficientStockError) as exc_info: reserve_stock("SKU001", 100) # 库存只有10 # 验证异常属性 assert exc_info.value.sku_id == "SKU001" assert exc_info.value.requested_qty == 100 assert exc_info.value.available_qty == 10 def test_network_timeout_retries(): """测试网络超时自动重试""" # 模拟前两次失败,第三次成功 mock_session = Mock() mock_session.get.side_effect = [ requests.exceptions.Timeout(), requests.exceptions.Timeout(), Mock(status_code=200, json=lambda: {"data": "ok"}) ] result = fetch_with_retry(mock_session, "https://api.com") assert result == {"data": "ok"} assert mock_session.get.call_count == 3 # 验证重试次数

关键点:1)用pytest.raises()精确捕获预期异常;2)验证异常实例的属性而非仅消息;3)对重试逻辑,必须验证调用次数和最终结果。我们要求所有异常处理分支的测试覆盖率≥95%,CI中用pytest-cov强制检查。

6. 常见问题与避坑指南:那些让你加班到凌晨的细节

6.1 “为什么我的except没生效?”——异常捕获失效的四大元凶

元凶一:异常类型不匹配
# 错误:想捕获KeyError,但实际抛出的是AttributeError data = {"name": "Alice"} try: print(data.name) # AttributeError: 'dict' object has no attribute 'name' except KeyError: # 永远不会触发 print("Key not found")

诊断:打印type(e)确认实际异常类型。用except (KeyError, AttributeError):或更宽泛的except Exception:

元凶二:异常在except块外抛出
# 错误:except块内的代码又抛异常,掩盖了原异常 try: risky_operation() except ValueError: logger.error("Log failed!") # 这里抛出OSError,原ValueError丢失 raise # 必须显式re-raise

解决方案:在except块内所有操作都用try-except包裹,或确保日志等操作绝对可靠。

元凶三:异步代码的异常陷阱
# 错误:async def中await的异常不会被外层try捕获 async def bad_async(): try: await some_async_func() # 这里抛出TimeoutError except TimeoutError: # 不会触发!因为await在另一个协程中 print("Handled") # 正确:在await处捕获 async def good_async(): try: await some_async_func() except TimeoutError: print("Handled")
元凶四:上下文管理器的静默失败
# 错误:with语句中__exit__方法返回True会抑制异常 class BadContextManager: def __exit__(self, exc_type, exc_val, exc_tb): return True # 这会吃掉所有异常! with BadContextManager(): raise ValueError("This will disappear!")

检查:确保__exit__只在明确要抑制异常时返回True,通常应返回FalseNone

6.2raiseraise ... from的哲学:何时该打断因果链?

raiseraise ... from的区别关乎错误溯源。看这个例子:

def load_config(): try: with open("config.json") as f: return json.load(f) except FileNotFoundError: # 场景1:用raise ... from保留原始异常 raise ConfigLoadError("Failed to load config") from None # 场景2:用raise ... from保留因果链 raise ConfigLoadError("Failed to load config") from e # 场景1的traceback: # ConfigLoadError: Failed to load config # 场景2的traceback: # ConfigLoadError: Failed to load config # +-- FileNotFoundError: [Errno 2] No such file or directory: 'config.json'

选择原则

  • from None:当原始异常是实现细节,对用户无意义(如内部缓存失效)
  • from e:当原始异常是根本原因,需保留完整调用链(如文件不存在导致配置加载失败)
  • 直接raise:当你要重新抛出同一个异常,不做任何修改

我在一个配置中心服务中严格区分:ConfigNotFoundErrorfrom e,因为运维需要知道是哪个文件缺失;而CacheInvalidateErrorfrom None,因为缓存失效是内部机制,用户只需知道“配置刷新失败”。

6.3 资源清理的终极方案:contextlib.suppress vs try-finally

contextlib.suppress适合“预期中的、可忽略的异常”,比如删除临时文件:

from contextlib import suppress import os # 删除文件,如果文件不存在也不报错 with suppress(FileNotFoundError): os.remove("/tmp/temp_file.log") # 等价于 try: os.remove("/tmp/temp_file.log") except FileNotFoundError: pass

绝不用于关键资源清理!比如:

# 危险:suppress会吃掉所有异常,包括OSError with suppress(OSError): f.close() # 如果close失败,资源泄漏! # 正确:用try-finally确保清理 f = open("file.txt") try: process(f) finally: f.close() # 即使process()抛异常,close也会执行

我的经验:suppress只用于“副作用操作”(如日志、清理),try-finally用于“必须执行的资源释放”。

6.4 性能陷阱:异常处理真的比if慢吗?

“异常处理很慢,应该用if校验代替”的说法是过时的。现代Python(3.8+)对try-except做了深度优化。基准测试显示:

  • try-except无异常时,性能与if几乎相同(差异<5%)
  • try-except有异常时,比if校验慢10-100倍(取决于异常类型)

结论
✅ 对罕见情况(如文件不存在、网络超时)用try-except——代码更清晰,性能无损
❌ 对高频检查(如循环中判断变量是否为None)用if——避免异常开销

我在一个高频交易系统中验证:对每秒10万次的订单价格校验,用if price > 0try: assert price > 0快37倍。但对每小时一次的配置加载,用try-except更安全简洁。

7. 实战案例:从零构建一个鲁棒的API客户端

7.1 需求分析:我们要对抗什么?

假设我们要写一个调用天气API的客户端,需应对:

  • 网络超时(requests.exceptions.Timeout
  • DNS解析失败(requests.exceptions.ConnectionError
  • API返回非200状态码(requests.exceptions.HTTPError
  • JSON解析失败(json.JSONDecodeError
  • 业务逻辑错误(如API返回{"error": "invalid_key"}

7.2 分层异常设计

# 自定义异常层次 class WeatherAPIError(Exception): """天气API基础异常""" class NetworkError(WeatherAPIError): """网络层异常""" class HTTPError(WeatherAPIError): """HTTP协议异常""" def __init__(self, status_code: int, message: str): self.status_code = status_code super().__init__(f"HTTP {status_code}: {message}") class BusinessError(WeatherAPIError): """业务层异常""" def __init__(self, error_code: str, message: str): self.error_code = error_code super().__init__(f"Business {error_code}: {message}")

7.3 带重试和熔断的客户端实现

import time import logging from functools import wraps from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type logger = logging.getLogger(__name__) def with_retry_and_circuit_breaker(max_attempts=3): """重试装饰器,集成熔断""" def decorator(func): @wraps(func) @retry( stop=stop_after_attempt(max_attempts), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((NetworkError, HTTPError)) ) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except NetworkError as e: logger.warning(f"Network error on attempt {wrapper.retry_state.attempt_number}: {e}") raise except HTTPError as e: if 500 <= e.status_code < 600: logger.warning(f"Server error {e.status_code} on attempt {wrapper.retry_state.attempt_number}") raise else: # 客户端错误不重试 raise BusinessError("client_error", str(e)) from e return wrapper return decorator class WeatherClient: def __init__(self, api_key: str, base_url: str = "https://api.weather.com"): self.api_key = api_key self.base_url = base_url self.session = requests.Session() self.session.headers.update({"Authorization": f"Bearer {api_key}"}) @with_retry_and_circuit_breaker(max_attempts=3) def get_forecast(self, city: str, days: int = 7) -> dict: url = f"{self.base_url}/forecast" params = {"city": city, "days": days} try: response = self.session.get(url, params=params, timeout=5) response.raise_for_status() # 抛出HTTPError # 解析JSON try: data = response.json() except json.JSONDecodeError as e: raise WeatherAPIError(f"Invalid JSON response: {e}") from e # 检查业务错误 if data.get("error"): raise BusinessError(data["error"]["code"], data["error"]["message"]) return data except requests.exceptions.Timeout: raise NetworkError("Request timeout") from None except requests.exceptions.ConnectionError as e: raise NetworkError(f"Connection failed: {e}") from None except requests.exceptions.HTTPError as e: raise HTTPError(response.status_code, str(e)) from None

7.4 使用示例与故障注入测试

# 正常使用 client = WeatherClient("your-api-key") try: forecast = client.get_forecast("Beijing", days=3) print(forecast["temperature"]) except BusinessError as e: # 处理业务错误,如API密钥无效 logger.error(f"
http://www.jsqmd.com/news/891876/

相关文章:

  • 【光波仿真实践】基于MATLAB的厄米特-高斯光束模式可视化与光强分析
  • 模拟电路版图工具PK:Synopsys Custom Compiler、LAYGO2、Berkeley BAG2、ALIGN、MAGICAL(包括维护时间)
  • SDL2核心函数到底怎么用?从SDL_Init到SDL_Quit,一篇讲透初始化与资源管理的最佳实践
  • 知识图谱补全技术赋能工业FMEA:从文本到可推理知识网络的实践
  • 关联规则挖掘实战:从超市货架到电商推荐的商业逻辑
  • WinThumbsPreloader:重新定义Windows资源管理效率的智能革命
  • 淄博汽车贴膜哪家好?临淄车主都在找的贴膜老店:完美车饰-15 年贴膜老店 - 资讯快报
  • 终于搞懂 XSS 为什么能盗号了:Cookie、Session、HttpOnly 一次讲明白
  • 从重复劳动到智能助手:如何用Auto.js实现Android自动化革命
  • 5分钟上手U-Net:用深度学习轻松实现医学图像细胞膜分割
  • Java实战:手把手教你用Spring Boot集成海康综合安防平台API(附完整代码)
  • 购物篮分析实战:用Apriori挖掘高价值商品关联规则
  • 4.2 咖啡师不需要十年功底,兼职一周上手
  • 国内游戏动画培训排名前十机构推荐2026 - 资讯快报
  • 如何通过 Python 调用 Taotoken 的多模型 API 快速构建应用
  • CS2_External游戏内存操作框架深度解析与实战指南
  • House of Cat
  • 手把手教你用Vivado和ZYNQ7000玩转PS与PL通信:一个GPIO控制的完整实战
  • AI工具协同失效诊断手册:用3个指标(响应熵值、上下文衰减率、意图偏移度)秒判工作流亚健康
  • 蓝桥杯单片机选手必看:STC15F2K60S2上DS18B20驱动移植与调试避坑指南
  • SQL 转 ER 图在线工具:一键自动生成实体关系ER图 + 系统整体ER图
  • 老旧设备系统兼容性完整指南:让过时硬件焕发新生
  • KityMinder脑图工具:5个超实用技巧让你工作效率翻倍
  • 多项式插值算法
  • 3分钟掌握BetterNCM安装器:一键解锁网易云音乐完整潜力
  • 面壁智能开源低比特大模型训练成果 BitCPM-CANN,推理阶段释放约 6 倍显存红利
  • 在ubuntu上配置taotoken作为python开发环境的默认大模型服务
  • 武汉圣擎航空:一站式机票酒店签证包车出行服务,高效省心出行优选 - 土星买买买
  • BiGRU-Attention与卡尔曼滤波融合的负面舆情预测模型实践
  • 3分钟掌握iOS应用签名:终极图形化工具完整指南