ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

捕获异常不是补丁,而是程序健壮性的设计起点

捕获异常不是补丁,而是程序健壮性的设计起点 1. 什么是“捕获异常”它不是错误处理的补丁而是程序健壮性的设计起点“捕获异常”这四个字听起来像程序员写完代码后临时打上的胶带——功能跑通了但怕出错赶紧加个 try-except 包一层。可我在做金融系统交易引擎、工业物联网边缘网关、以及高并发电商库存服务这十多年里反复验证了一个事实真正稳定的系统不是靠“没出错”撑起来的而是靠“出错时知道怎么收场”立住的。捕获异常从来不是事后补救它是和变量声明、函数设计、接口定义同等重要的第一等公民。你写的每一行业务逻辑都应该默认它会失败你加的每一个 except都不是在兜底而是在定义“失败时系统该呈现什么状态、该释放什么资源、该通知谁、该记录什么线索”。很多人一看到热搜词里满屏的 “selected model is at capacity. please try a different model.”、“exception: couldn’t start the app because http://127.0.0.1:7860/gradio_api/”、“java.lang.NoClassDefFoundError” 就慌了神以为是环境配错了、依赖漏装了、或者模型服务器崩了。但真相往往是这些错误本该被提前捕获、分类、降级而不是直接炸穿到用户界面上。比如那个 Gradio 的 7860 端口报错背后可能是 GPU 显存耗尽也可能是模型加载超时还可能是反向代理配置失效——但用户看到的只是一行冰冷的 URL 错误。如果开发者在启动服务前就用 try/except 包裹了模型加载和端口绑定并在 except 里明确区分了 OSError端口占用、RuntimeError显存不足、ImportError模型文件缺失那运维同学收到的告警日志里就能直接看到 “ERROR [ModelLoader] CUDA out of memory on device 0”而不是翻三小时日志才定位到是某次批量推理把显存吃爆了。再看 Oracle GoldenGate 的 OGG-15051 错误它常出现在数据同步链路中断时。很多 DBA 习惯性重启进程却忽略了这个错误背后可能关联着源库归档日志被清理、目标表结构变更未同步、甚至网络抖动导致的连接重置。一个设计良好的捕获机制应该在捕获到 OGG-15051 时不仅记录错误码还要主动抓取当前的检查点位置、最近 5 条应用日志、以及源/目标库的 SCN 号把这些信息打包进告警消息。这样值班工程师打开企业微信看到的就不是“OGG 进程挂了”而是“OGG-15051rep_busi.prm 在 SCN 123456789 处中断源库归档日志已清理至 123456700建议立即检查归档保留策略”。这才是捕获异常的真正价值把模糊的“出错了”翻译成精确的“哪里错了、为什么错、现在该怎么办”。我见过太多团队把异常捕获写成“万能 catch”except Exception as e:一把梭哈然后print(e)或者logging.error(str(e))就完事。这就像给消防栓装了个装饰性盖子——看着有真着火时根本打不开。真正的捕获必须分层底层模块暴露原始异常如OSError,ValueError中间层做语义转换把OSError(112, Host is down)转成NetworkUnreachableError上层业务逻辑则只处理自己能决策的异常比如支付失败时触发退款流程对无法处理的如数据库连接池彻底枯竭必须原样抛出或转为ServiceUnavailableError让网关统一熔断。这种分层不是为了炫技而是为了让错误沿着调用栈向上“说话”让每个环节都只听懂自己该听的部分。所以“捕获异常”的本质是建立一套程序内部的“错误语言体系”而 try/except/else/finally就是这套语言的语法糖。2. 核心语法拆解为什么需要 else 和 finally它们不是装饰而是控制流的锚点很多人学 Python 异常处理只记住了try...except这个骨架觉得else和finally是可有可无的锦上添花。我在给新入职工程师做 Code Review 时90% 的异常处理问题都出在这两个关键字的误用或弃用上。它们绝不是语法糖而是控制流中不可替代的锚点各自承担着截然不同的责任边界。2.1 else 子句唯一能证明“业务逻辑干净执行”的公证人else子句的语义非常纯粹它只在 try 块中没有任何异常抛出时才会执行。注意这里的关键是“没有任何异常”不是“没有被 except 捕获的异常”。这意味着else是整个 try 块成功执行的铁证。我把它比作银行柜台的“业务办结章”——只有当所有验资、签字、复核流程全部无声无息走完柜员才会盖下这枚章。如果把本该放在else里的逻辑硬塞进try里就等于让柜员在验资过程中就提前盖章一旦后续签字环节出错章就盖错了。举个典型反例处理用户上传的 Excel 文件。# ❌ 错误示范把文件解析逻辑混在 try 里 try: workbook load_workbook(file_path) # 可能 FileNotFoundError sheet workbook.active data [] for row in sheet.iter_rows(values_onlyTrue): data.append(row) # ✅ 这里才是真正的业务逻辑清洗、校验、入库 clean_data validate_and_clean(data) save_to_database(clean_data) except FileNotFoundError: log_error(文件不存在) except ValueError as e: log_error(fExcel 格式错误: {e})问题在哪validate_and_clean和save_to_database这两个核心业务操作被裹在try里。万一save_to_database抛出IntegrityError主键冲突它会被最外层的except ValueError漏掉因为IntegrityError不是ValueError的子类。更糟的是如果validate_and_clean里有个隐藏 bug 导致TypeError这个错误也会被吞掉你永远不知道是 Excel 解析错了还是业务规则写错了。✅ 正确写法try: workbook load_workbook(file_path) # 可能 FileNotFoundError sheet workbook.active data [] for row in sheet.iter_rows(values_onlyTrue): data.append(row) except FileNotFoundError: log_error(文件不存在) return except ValueError as e: log_error(fExcel 格式错误: {e}) return else: # ✅ 只有到这里才能 100% 确认文件存在、格式正确、数据已读取 # 所有后续操作都是纯粹的业务逻辑不该被文件 I/O 异常干扰 try: clean_data validate_and_clean(data) save_to_database(clean_data) except ValidationError as e: log_error(f业务校验失败: {e}) send_alert_to_business_team() except DatabaseError as e: log_error(f数据库写入失败: {e}) trigger_compensating_transaction() # 触发补偿事务看到区别了吗else划清了“资源获取”和“业务处理”的楚河汉界。else里的try是另一层独立的异常处理专门应对业务逻辑自身的风险。这种嵌套不是代码变复杂了而是责任更清晰了文件读取失败是运维问题业务校验失败是产品规则问题数据库写入失败是数据一致性问题。每个问题都有自己的处理路径互不污染。2.2 finally 子句程序退出前最后的“守门人”如果说else是成功的公证人finally就是失败时的守门人。它的执行条件极其刚性无论 try 块是正常结束、被 except 捕获后退出还是被未捕获的异常中断甚至遇到 return、break、continuefinally 都会无条件执行。这是 Python 提供的、最可靠的资源清理机制。我曾经维护过一个实时股票行情推送服务它需要维持一个 WebSocket 连接并在内存中缓存最近 1000 笔成交数据。早期版本的代码是这样的# ❌ 危险示范资源清理依赖 except def connect_and_stream(): ws websocket.create_connection(wss://market.example.com) try: while True: msg ws.recv() process_message(msg) except websocket.WebSocketConnectionClosedException: ws.close() # 只有在这里才关连接 log_info(WebSocket 连接关闭) except KeyboardInterrupt: ws.close() # 用户 CtrlC 时也关 log_info(手动停止服务)上线后第三天监控报警内存泄漏连接数暴增。排查发现当网络抖动导致ws.recv()抛出OSError(104, Connection reset by peer)时这个异常没有被except捕获因为只写了WebSocketConnectionClosedException程序直接崩溃ws.close()根本没执行成千上万个 socket 句柄就这么悬在操作系统里。这就是不使用finally的代价——你永远无法穷举所有可能导致程序退出的路径。✅ 正确写法def connect_and_stream(): ws None try: ws websocket.create_connection(wss://market.example.com) while True: msg ws.recv() process_message(msg) except websocket.WebSocketConnectionClosedException: log_info(WebSocket 连接关闭) except KeyboardInterrupt: log_info(手动停止服务) except Exception as e: log_error(f未知错误: {type(e).__name__}: {e}) finally: # ✅ 无论发生什么这里都必须执行 if ws and ws.connected: ws.close() log_info(WebSocket 连接已安全关闭) # 同时清理内存缓存 clear_local_cache() log_info(本地缓存已清空)finally保证了连接关闭和缓存清理这两件事成为程序生命周期的“最后防线”。它不关心错误类型不参与业务决策只做一件事确保该释放的资源一分不剩地交还给系统。这也是为什么with语句上下文管理器如此重要——它的底层实现就是编译器自动为你生成了try...finally结构。当你写with open(file.txt) as f:Python 实际上在背后帮你写了f open(file.txt) try: # 你的代码 finally: f.close()所以记住一条铁律任何需要显式释放的资源文件句柄、数据库连接、网络 socket、锁、GPU 内存其释放逻辑必须放在 finally 中或者交给 with 语句。这不是最佳实践这是生存法则。3. 异常分类与捕获策略从“万能 Exception”到“精准手术刀”新手最容易犯的错误就是写except Exception as e:。这就像医生面对病人不问症状、不查血常规、不做 CT直接开了一堆广谱抗生素。它能“治好”一部分病但更多时候是掩盖病情、延误治疗甚至产生耐药性。在生产环境中粗放的异常捕获是技术债的温床。3.1 Python 异常层级的本质它是一张描述“失败原因”的语义地图Python 的异常类不是随意堆砌的它是一个精心设计的继承树根节点是BaseException我们日常打交道的Exception是它的子类。这张树的结构本身就是对失败原因的分类学SystemExit,KeyboardInterrupt,GeneratorExit这些是程序生命周期事件不是错误绝不应该被常规 except 捕获。捕获KeyboardInterrupt会导致用户无法用 CtrlC 退出程序这是严重的设计缺陷。Exception这是所有“程序错误”的基类。它下面又分两大支内置错误Built-in ExceptionsValueError,TypeError,IOError,OSError,KeyError,IndexError等。它们描述的是 Python 解释器或标准库在执行时遇到的、与具体业务无关的底层问题。比如int(abc)抛ValueErrorlist[100]抛IndexError。自定义异常User-defined Exceptions这是开发者构建领域语义的画布。你应该基于Exception或其子类如ValueError创建自己的异常比如InsufficientBalanceError,InvalidOrderStatusError,PaymentGatewayTimeoutError。理解这个层级关键在于明白捕获越具体的异常你的处理逻辑就越精准程序的可预测性就越强。捕获OSError可以处理所有系统级 I/O 错误但它无法区分“磁盘满了”OSError(28, No space left on device)和“权限不足”OSError(13, Permission denied)。而捕获OSError的子类PermissionError就能专门处理后者。3.2 实战捕获策略三层过滤网模型我给自己团队定了一条硬性规范所有生产代码的异常处理必须遵循“三层过滤网”模型。这不是教条而是经过无数次线上事故淬炼出来的经验。第一层防御性捕获Preventive Catch目标拦截那些本可以避免、且有明确修复路径的常见错误。 适用场景外部输入、第三方 API 调用、文件/网络 I/O。 核心原则捕获最具体的异常并提供即时、可操作的反馈。# ✅ 示例调用外部支付网关 try: response requests.post( https://api.payment-gateway.com/v1/charge, jsonpayload, timeout10 ) response.raise_for_status() # 这会抛出 HTTPError except requests.exceptions.Timeout: # ✅ 具体异常网络超时 log_warning(支付网关请求超时将重试) retry_charge(payload, max_retries2) except requests.exceptions.ConnectionError: # ✅ 具体异常连接被拒绝或 DNS 失败 log_error(无法连接到支付网关请检查网络配置) send_alert_to_ops(Payment Gateway Unreachable) except requests.exceptions.HTTPError as e: # ✅ 具体异常HTTP 状态码错误 if response.status_code 400: log_error(f支付参数错误: {response.json().get(message, Unknown)}) raise InvalidPaymentRequestError from e elif response.status_code 401: log_error(支付网关认证失败请检查 API Key) raise PaymentAuthFailedError from e else: log_error(f支付网关返回非预期状态码: {response.status_code}) raise PaymentGatewayError from e注意raise ... from e的用法。它不是简单地重新抛出而是建立了异常链Exception Chaining让原始的HTTPError成为新异常的__cause__。这样当最终的日志或 Sentry 上报时你能同时看到“业务层的 InvalidPaymentRequestError”和“底层的 requests.exceptions.HTTPError”形成完整的故障链路图。第二层恢复性捕获Recovery Catch目标处理那些可以降级、可以补偿、可以优雅退化的错误。 适用场景非核心功能失败、缓存失效、异步任务队列积压。 核心原则捕获业务异常执行预案保证主流程不中断。# ✅ 示例用户头像上传CDN 分发失败时降级为本地存储 try: cdn_url upload_to_cdn(user_avatar_file) except CDNUploadFailedError as e: # ✅ 业务异常CDN 服务暂时不可用 log_warning(fCDN 上传失败降级为本地存储: {e}) # 执行降级方案 local_url save_to_local_storage(user_avatar_file) user.avatar_url local_url user.save() # 同时异步重试 CDN 上传 async_retry_cdn_upload.delay(user_id, user_avatar_file) else: # ✅ CDN 上传成功 user.avatar_url cdn_url user.save()第三层兜底性捕获Fallback Catch目标作为最后一道防线捕获所有未被前两层处理的、意料之外的错误防止程序崩溃并留下关键诊断线索。 适用场景顶层入口函数如 Web 请求 handler、CLI 主函数。 核心原则捕获Exception但绝不静默吞掉必须记录完整上下文并做出明确响应。# ✅ 示例FastAPI 的全局异常处理器 app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): # ✅ 获取完整 traceback import traceback tb_str traceback.format_exc() # ✅ 记录到结构化日志包含 request ID, user ID, timestamp logger.error( Unhandled Exception, extra{ request_id: request.state.request_id, user_id: getattr(request.state, user_id, anonymous), error_type: type(exc).__name__, error_message: str(exc), traceback: tb_str, path: request.url.path } ) # ✅ 返回友好的用户提示但绝不泄露敏感信息 return JSONResponse( status_code500, content{ success: False, message: 服务器开小差了请稍后再试, request_id: request.state.request_id # 提供给用户用于客服查询 } )重点来了这个兜底except Exception只允许出现在整个调用栈的最顶端。它像一个巨大的漏斗把所有漏网之鱼都收集起来但它的存在恰恰是为了让你能快速发现——哪些异常本该被前两层捕获却被遗漏了。每一次这个兜底被触发都是一次代码质量的警报。4. 实操避坑指南那些文档里不会写的“血泪教训”书本和教程教会你语法但只有踩过坑才知道哪些地方埋着雷。以下是我和团队在过去十年里用无数个不眠之夜换来的实操心得。它们不性感不炫技但每一条都能帮你少掉几根头发。4.1 “空 except” 是代码界的“黑洞”它会吞噬一切光和调试时间# ❌ 绝对禁止 try: do_something_risky() except: pass # 或者更糟print(oops)这个except:不带任何异常类型是 Python 里最危险的语法之一。它不仅捕获Exception还会捕获SystemExit,KeyboardInterrupt,GeneratorExit。这意味着当你想用 CtrlC 停止一个死循环脚本时它会一声不吭地继续跑下去。更可怕的是它会吞掉所有SyntaxError在导入模块时、MemoryError内存耗尽时等致命错误让你的程序在无声中崩溃连日志都不留一行。✅ 正确做法永远显式写出你要捕获的异常类型。# ✅ 宁可多写几行也要明确 try: result risky_operation() except (ValueError, TypeError) as e: handle_input_error(e) except ConnectionError as e: handle_network_error(e) # 如果真需要捕获所有也请用 Exception并立刻记录 except Exception as e: logger.critical(Unexpected error, exc_infoTrue) raise # 或者 re-raise4.2 日志记录的黄金法则exc_infoTrue是你的命脉很多团队的日志里只看到ERROR: An error occurred然后就没有然后了。这就像医生只告诉你“你病了”却不告诉你是什么病、在哪里病、有多严重。logging.error(An error occurred)只记录了字符串丢失了最关键的traceback。✅ 正确做法在记录异常时必须传入exc_infoTrue。# ❌ 错误只记录了错误消息 logger.error(fFailed to process order {order_id}: {str(e)}) # ✅ 正确记录了完整的堆栈跟踪 logger.error(fFailed to process order {order_id}, exc_infoTrue) # ✅ 更佳结合 structured logging logger.error( Order processing failed, extra{ order_id: order_id, user_id: user_id, error_type: type(e).__name__ }, exc_infoTrue )exc_infoTrue会自动将当前的sys.exc_info()即异常类型、异常值、traceback 对象注入日志。有了完整的 traceback你才能一眼看出错误发生在payment_service.py的第 42 行而不是在main.py的第 100 行。这是线上故障排查的基石。4.3 “裸 raise” vs “raise from”别让错误链变成一团乱麻# ❌ 模糊的错误链 try: result call_external_api() except requests.exceptions.Timeout: raise RuntimeError(Payment service timeout) # ✅ 清晰的错误链 try: result call_external_api() except requests.exceptions.Timeout as e: raise PaymentTimeoutError(Payment gateway did not respond) from e第一种写法RuntimeError的__cause__是None你只能看到一个孤立的错误。第二种写法PaymentTimeoutError的__cause__是原始的requests.exceptions.Timeout形成了PaymentTimeoutError→requests.exceptions.Timeout的清晰因果链。在 Sentry 或 ELK 里这个链会自动展开让你无需翻代码就能看到是支付网关超时导致了我们的业务超时错误。4.4 finally 里的异常它会“杀死” try 块里抛出的异常这是一个极其隐蔽的陷阱。finally块里如果抛出异常它会完全取代try块里原本要抛出的异常。def dangerous_finally(): try: raise ValueError(Original error) finally: raise RuntimeError(Finally error) # ✅ 这个异常会覆盖上面的 ValueError # 调用结果只会看到 RuntimeError: Finally error # ValueError: Original error 彻底消失这在资源清理逻辑中尤其危险。比如你在finally里关闭一个已经损坏的数据库连接conn.close()本身可能抛出OperationalError这个新异常就会把原始的业务异常比如IntegrityError给覆盖掉。✅ 安全写法在finally里做清理时用try/except包裹清理操作并记录其异常但绝不让它逃逸。def safe_cleanup(): conn get_db_connection() try: # 业务逻辑 execute_business_sql(conn) except IntegrityError as e: logger.error(Business rule violation, exc_infoTrue) raise finally: # ✅ 安全的清理 try: if conn and not conn.closed: conn.close() except Exception as cleanup_e: # ✅ 记录清理异常但不抛出 logger.warning(Failed to close database connection, exc_infoTrue)4.5 异常处理的性能陷阱不要在 hot path 上做昂贵操作异常处理本身是有开销的尤其是在高频调用的代码路径hot path上。try/except块的创建、except子句的匹配、traceback对象的构建都不是零成本。❌ 错误示范用异常来控制正常流程EAFP vs LBYL 的误用# 这是典型的“用异常做控制流”性能极差 for item in large_list: try: value item[key] # 可能 KeyError except KeyError: value default✅ 正确做法对于高频、可预测的访问优先用dict.get()或hasattr()等廉价检查LBYL - Look Before You Leap。# ✅ 快速、安全 for item in large_list: value item.get(key, default) # ✅ 或者如果 key 存在是常态缺失是真异常则用 EAFP try: value item[key] except KeyError: # 这里处理的是真正的异常情况而非常态 handle_missing_key(item)判断标准很简单如果某个“失败”在你的业务场景中是常态比如用户提交的表单里某个字段经常为空那就用 LBYL如果它是真正的意外比如配置文件里本该存在的 key 突然没了那就用 EAFP。混淆这两者是性能问题的根源。5. 真实世界异常案例深度复盘从报错信息到根因定位网络热搜里那些五花八门的错误信息不是噪音而是系统健康状况的实时脉搏。我们来挑几个典型手把手拆解如何从一行报错定位到代码深处。5.1 案例一“selected model is at capacity. please try a different model.”这个错误在大模型 API 服务中高频出现。表面看是模型服务器满了但背后可能有多个层次的问题。第一步确认错误来源它是来自你调用的第三方 API如 OpenAI、Anthropic还是你自建的模型服务如 vLLM、Text Generation Inference查看 HTTP 响应头X-Model-Name或响应体中的model字段确认具体是哪个模型。第二步分层诊断应用层你的代码检查是否在短时间内发起了过多并发请求超过了你购买的配额。用logging.info记录每次请求的model_name和timestamp观察是否集中爆发。网关层如 Nginx、API Gateway检查是否有请求被限流429 Too Many Requests或者上游健康检查失败导致流量全部打到一个节点。模型服务层vLLM查看 vLLM 的 metricsPrometheus重点关注vllm:gpu_cache_usage_ratioGPU 缓存使用率和vllm:queue_size等待队列长度。如果queue_size持续 100说明请求积压严重。基础设施层检查 GPU 显存nvidia-smi确认是否真的被占满检查 CPU 和内存确认是否因资源争抢导致调度延迟。第三步捕获与降级import time from tenacity import retry, stop_after_attempt, wait_exponential retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10), reraiseTrue ) def call_llm_api(prompt, modelgpt-4): try: response requests.post( https://api.your-llm-service.com/v1/chat/completions, json{model: model, messages: [{role: user, content: prompt}]}, timeout30 ) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if response.status_code 429: # ✅ 精准捕获容量错误 error_detail response.json().get(error, {}).get(message, ) if at capacity in error_detail.lower(): log_warning(fModel {model} is at capacity. Retrying...) # 可以在此处切换模型 if model gpt-4: new_model gpt-3.5-turbo else: new_model gpt-4 # 更新参数重试 raise raise这个例子展示了如何将一个模糊的业务错误转化为可编程的、可重试的、可降级的信号。5.2 案例二“error ogg-15051 oracle goldengate delivery, rep_busi.prm”OGG-15051 是 GoldenGate 的经典错误含义是“Replicat 进程无法应用 trail 文件中的记录”。它通常指向数据不一致。关键诊断步骤Step 1检查 Replicat 状态ggsci info rep_busi # 查看状态是 ABENDED 还是 RUNNING以及最后的 checkpointStep 2查看详细错误日志tail -100 /path/to/ggs/dirrpt/rep_busi.rpt # 关键线索Look for SQL error or ORA- codesStep 3定位到具体的 SQLOGG 日志里会记录失败的 SQL例如2024-05-20 10:12:34 ERROR OGG-01296 Error mapping table SCOTT.EMP to SCOTT.EMP: No unique key is defined for table SCOTT.EMP.这说明目标表缺少主键或唯一索引OGG 无法定位要更新的行。捕获与自动化修复虽然 OGG 本身不支持 Python 异常处理但你可以用 Shell 脚本监控其日志#!/bin/bash # monitor_ogg.sh LOG_FILE/path/to/ggs/dirrpt/rep_busi.rpt ERROR_PATTERNOGG-01296|OGG-01163 if grep -q $ERROR_PATTERN $LOG_FILE; then # ✅ 捕获到关键错误 LAST_ERROR$(tail -20 $LOG_FILE | grep -E $ERROR_PATTERN | tail -1) echo OGG ERROR DETECTED: $LAST_ERROR | mail -s OGG Alert opscompany.com # ✅ 自动化修复检查目标表约束 sqlplus / as sysdba EOF SELECT constraint_name, constraint_type FROM dba_constraints WHERE table_nameEMP AND ownerSCOTT AND constraint_type IN (P,U); EOF fi这个脚本就是 OGG 的“外部异常处理器”它把数据库层面的错误转化为了运维可操作的事件。5.3 案例三“exception: couldnt start the app because http://127.0.0.1:7860/gradio_api/”Gradio 的这个错误几乎总是源于端口冲突或服务未启动。系统性排查清单端口占用检查# Linux/Mac lsof -i :7860 # Windows netstat -ano | findstr :7860Gradio 进程检查ps aux | grep gradio # 或者如果你用的是 pipenv/poetry检查虚拟环境是否激活依赖版本冲突gradio的新版本有时会与旧版fastapi或starlette不兼容。运行pip list | grep -E (gradio|fastapi|starlette)对照官方文档的兼容矩阵。防火墙/SELinux 在 CentOS/RHEL 上sestatus查看 SELinux 状态sudo setsebool -P httpd_can_network_connect 1可能是必需的。预防性捕获在你的启动脚本中import subprocess import time import requests def start_gradio_app(): # ✅ 启动前检查端口 if is_port_in_use(7860): log_error(Port 7860 is already in use. Please kill the process or change port.) return False # ✅ 启动 Gradio proc subprocess.Popen([python, app.py]) # ✅ 等待并健康检查 for i in range(30): # 等待最多 30 秒 try: response requests.get(http://127.0.0.1:7860/gradio_api/, timeout5) if response.status_code 200: log_info(Gradio app started successfully on port 7860) return True except requests.exceptions.RequestException: time.sleep(1) log_error(Gradio app failed to start within timeout) proc.terminate() return False这个例子说明最好的异常捕获往往发生在错误发生之前。6. 工程化实践将异常处理融入 CI/CD 和 SRE 流程异常处理不能只停留在代码层面它必须成为整个软件交付流水线的一部分。否则再漂亮的try/except也只是一朵温室里的花。6.1 在单元测试中强制覆盖异常路径很多团队的单元测试只覆盖 happy path快乐路径对异常路径视而不见。这是最大的盲区。你应该用pytest的pytest.raises和unittest.mock.patch主动构造异常场景。# test_payment_service.py import pytest from unittest.mock import patch, MagicMock from payment_service import process_payment def test_process_payment_timeout(): # ✅ 模拟 requests.post 抛出 Timeout with patch(payment_service.requests.post) as mock_post: mock_post.side_effect requests.exceptions.Timeout(Gateway timeout) # ✅ 断言它会抛出我们定义的业务异常 with pytest.raises(PaymentTimeoutError): process_payment(order_id123, amount100.0) # ✅ 断言日志被正确记录 assert Payment gateway timeout in caplog.text def test_process_payment_invalid_card(): # ✅
返回列表