2. 异常捕获基础:Python中的try-except-finally机制

异常处理,说白了就是给代码上「保险」。

我在模型部署这行干了快十年,见过太多因为没处理好异常,导致整个推理服务挂掉的惨案。你想想看,一个线上服务跑得好好的,突然因为某个样本格式不对,整个进程就崩了——这谁受得了?

所以今天咱们就来聊聊Python里最基础的异常处理机制:try-except-finally。别觉得它简单,用好了是护身符,用不好就是埋雷。

2.1 为什么需要异常处理?

先问个问题:你的模型推理代码,真的能处理所有输入吗?

我遇到过的情况是这样的——有一次部署一个图像分类模型,线上突然报错。查了半天,发现是某张图片损坏了,cv2.imread()返回了None,然后后续的预处理直接崩了。整个推理服务挂了5分钟,那叫一个惨。

异常处理要解决的核心问题就三个:

  • 防止程序崩溃:遇到错误别直接挂掉,给个优雅的退路
  • 记录错误信息:出错了得知道为什么,不然怎么修?
  • 保证资源释放:文件句柄、GPU显存、网络连接,这些都得妥善处理

核心原则:异常处理不是为了掩盖错误,而是为了在错误发生时,系统还能保持可控状态。

2.2 try-except 的基本用法

先看个最简单的例子。假设你写了个模型推理函数:

def predict(model, input_data):
    try:
        result = model(input_data)
        return result
    except:
        print("推理出错了")
        return None

这段代码能跑,但问题很大。为什么?

因为except:捕获了所有异常。包括KeyboardInterrupt(用户按Ctrl+C)、SystemExit(系统退出)这些你根本不该捕获的东西。

警告:裸写except:等于告诉Python「所有错误我都接着」,这很危险。我曾经见过有人这么写,结果程序死循环了都停不下来——因为Ctrl+C也被捕获了。

正确的做法是:捕获你能处理的特定异常

def predict(model, input_data):
    try:
        result = model(input_data)
        return result
    except RuntimeError as e:
        print(f"模型推理运行时错误: {e}")
        return None
    except ValueError as e:
        print(f"输入数据格式错误: {e}")
        return None

这样写,每个异常都有对应的处理逻辑。出错了你一眼就知道是哪里的问题。

2.3 捕获特定异常 vs 通用异常

这里有个常见的困惑:到底该捕获特定异常,还是用通用异常?

我的建议是——分层处理

异常类型 适用场景 示例
特定异常 你知道可能出什么错,并且能处理 FileNotFoundError → 换个路径重试
通用异常 兜底保护,防止未知错误导致崩溃 Exception → 记录日志后返回默认值

看个实际例子。我在部署ONNX模型时,经常这么写:

def load_onnx_model(model_path):
    try:
        session = ort.InferenceSession(model_path)
        return session
    except FileNotFoundError:
        print(f"模型文件不存在: {model_path}")
        # 尝试从备份路径加载
        backup_path = model_path.replace("/models/", "/models_backup/")
        return load_onnx_model(backup_path)
    except Exception as e:
        print(f"未知错误加载模型: {e}")
        # 记录日志,通知运维
        log_error(f"模型加载失败: {model_path}, 错误: {e}")
        return None

你看,FileNotFoundError是特定异常,我知道它可能发生,而且知道怎么处理——换个路径试试。Exception是通用异常,用来兜底,防止其他意外情况导致程序崩溃。

小技巧:通用异常要放在最后,特定异常放在前面。因为Python是按顺序匹配except块的,一旦匹配到就不会继续往下走了。

2.4 finally 的妙用

finally块是干嘛用的?说白了就是「不管出不出错,这段代码都得执行」。

我印象最深的一次——有个同事写模型推理服务,每次推理前分配GPU显存,推理完忘了释放。结果跑了半天,显存爆了,服务挂了。这就是典型的资源泄漏。

finally就是解决这个问题的:

def safe_inference(model, input_data):
    gpu_memory = allocate_gpu_memory(1024)  # 分配1GB显存
    try:
        result = model(input_data)
        return result
    except RuntimeError as e:
        print(f"推理失败: {e}")
        return None
    finally:
        release_gpu_memory(gpu_memory)  # 不管成功失败,都得释放
        print("GPU显存已释放")

注意:finally里的代码一定会执行,即使tryexcept里有return语句。这是Python的硬性规定。

经验之谈:在模型部署中,finally最常用的场景就是释放资源——关闭文件、释放显存、断开数据库连接。这些事忘了做,迟早要出大问题。

2.5 实战:一个完整的异常处理模板

最后,给你一个我在项目中常用的模板。这个模板处理了模型推理中最常见的几种异常:

def model_inference_pipeline(model, input_data, timeout=30):
    """
    带完整异常处理的模型推理管道
    """
    import signal
    
    # 设置超时处理
    def timeout_handler(signum, frame):
        raise TimeoutError("推理超时")
    
    signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(timeout)
    
    try:
        # 1. 输入校验
        if input_data is None:
            raise ValueError("输入数据为空")
        
        # 2. 数据预处理
        processed = preprocess(input_data)
        
        # 3. 模型推理
        result = model(processed)
        
        # 4. 后处理
        output = postprocess(result)
        
        return output
        
    except ValueError as e:
        print(f"输入数据异常: {e}")
        return {"error": str(e), "code": 400}
        
    except TimeoutError as e:
        print(f"推理超时: {e}")
        return {"error": "推理超时", "code": 504}
        
    except MemoryError as e:
        print(f"内存不足: {e}")
        # 清理缓存
        clear_cache()
        return {"error": "内存不足", "code": 503}
        
    except Exception as e:
        print(f"未知错误: {e}")
        log_error(f"模型推理异常: {e}")
        return {"error": "内部错误", "code": 500}
        
    finally:
        # 取消超时定时器
        signal.alarm(0)
        # 释放临时资源
        release_temp_resources()

这个模板的好处是:

  • 每个异常都有明确的处理逻辑
  • 返回统一的错误格式,方便前端处理
  • finally保证资源一定释放
  • 超时控制防止推理卡死

注意except Exception不会捕获KeyboardInterruptSystemExit,因为它们继承自BaseException而不是Exception。这是Python特意设计的,防止你误吞这些关键信号。

好了,关于try-except-finally的基础就聊到这儿。记住一句话:异常处理不是锦上添花,而是雪中送炭。在模型部署这种高可靠性要求的场景下,每一行代码都值得你多想想——「如果这里出错了,怎么办?」

下一节,咱们聊聊更高级的异常处理技巧,包括自定义异常和异常链。到时候见。