自定义异常

一句话概述

Python 允许开发者通过继承内置异常类来创建自己的异常类型。自定义异常让错误信息更具语义化,能精准描述业务逻辑层面的错误(如"训练数据不足"、"模型未初始化"),提升代码可读性和调试效率。

💡 核心要点:①自定义异常通过 class MyError(Exception): pass 继承 Exception 基类创建 ②用 raise 关键字主动抛出异常 ③自定义异常可携带额外信息(错误码、上下文数据)便于调试 ④在 except 中像内置异常一样捕获自定义异常

教学与演示

一、为什么需要自定义异常——内置异常不够用了

是什么:Python 内置的异常类型(ValueErrorTypeError 等)描述的是语言层面的错误。但当你的程序有业务逻辑层面的错误时(比如"用户年龄不能超过 150 岁"、"模型尚未加载不能推理"),内置异常无法精确表达。自定义异常填补了这个空白。

大白话 就像医院里需要分科室——"不舒服"(Exception)太笼统了,"骨折"(ValueError)和"发烧"(TypeError)是内置的科室。但如果病人是"AI 训练过拟合"这种新病,就需要开设"自定义科室"(自定义异常)。这样医生一看科室名字就知道问题大概是什么。

为什么:在大型项目中,精准的异常类型能极大提升调试效率。当系统抛出 ModelNotLoadedError 而不是泛泛的 RuntimeError 时,你立刻就知道是模型加载环节出了问题。对于 AI 系统,训练失败、数据不足、GPU OOM 等都需要专用的异常类型。

怎么做

import numpy as np

# ===== 为什么要自定义异常:内置异常不够精确 =====

# 场景:AI 模型推理系统
# 内置异常能表达的问题:
errors_builtin = {
    "ValueError": "输入数据格式不对(太笼统,不知道是哪个环节)",
    "TypeError": "传入了错误的数据类型(太笼统)",
    "RuntimeError": "运行时出错了(最笼统的异常)",
}

# 自定义异常能精确表达的问题:
errors_custom = {
    "ModelNotLoadedError": "模型尚未加载,不能推理 → 知道是初始化问题",
    "InputShapeMismatchError": "输入维度(256,)不匹配模型期望(512,) → 知道是预处理问题",
    "InsufficientDataError": "训练数据仅100条,最少需要1000条 → 知道是数据集问题",
    "GPUOutOfMemoryError": "GPU 显存不足,当前需要 4GB 但仅剩 2GB → 知道是硬件问题",
}

print("【内置异常 vs 自定义异常】\n")
print("内置异常:")
for k, v in errors_builtin.items():
    print(f"  {k}: {v}")

print("\n自定义异常(语义更精准):")
for k, v in errors_custom.items():
    print(f"  {k}: {v}")

# 量化对比:用 numpy 展示
semantic_scores = np.array([2, 2, 1])  # 内置异常的语义精准度(1-10)
custom_scores = np.array([9, 8, 9, 8])  # 自定义异常的语义精准度
print(f"\n内置异常平均语义精准度:{np.mean(semantic_scores):.1f}/10")
print(f"自定义异常平均语义精准度:{np.mean(custom_scores):.1f}/10")

什么用:在 AI 系统中,自定义异常可以实现分层错误处理——数据层的错误抛 DataPipelineError,模型层的错误抛 ModelError,服务层的错误抛 ServiceError。上层只需捕获对应层次的异常,就能精准定位问题并采取不同策略(重试、降级、报警)。

二、创建自定义异常——从零开始造"错误类型"

是什么:创建自定义异常只需 3 步:(1) 定义一个类继承 Exception;(2) 通过 __init__ 方法接收错误信息和额外参数;(3) 用 raise 关键字主动抛出。自定义异常类是普通 Python 类的子类,可以拥有任意属性和方法。

大白话 就像你去定制一件 T 恤——基类 Exception 是白 T 恤的模板,你继承它后,可以添加自己的"印花"(额外属性、错误码),做出一件能表达特定错误的定制 T 恤。raise 就是把这件 T 恤"举起来"让大家看到。

为什么:好的自定义异常不仅告诉你"出错了",还告诉你"怎么错的"和"怎么修"。通过携带 extra info(如期望值和实际值的对比、出错的上下文数据),能省去大量查日志的时间。

怎么做

import numpy as np

# ===== 自定义异常的创建和使用 =====

# 步骤1:定义自定义异常类(继承 Exception)
class DataValidationError(Exception):
    """数据验证失败异常"""
    def __init__(self, message, field_name=None, invalid_value=None):
        super().__init__(message)
        self.field_name = field_name      # 哪个字段出错
        self.invalid_value = invalid_value # 出错的值是什么

class ModelNotReadyError(Exception):
    """模型未就绪异常"""
    def __init__(self, message, model_name=None):
        super().__init__(message)
        self.model_name = model_name

# 步骤2:在业务代码中使用自定义异常
def validate_training_data(samples):
    """验证训练数据是否满足最低要求"""
    if len(samples) < 10:
        raise DataValidationError(
            f"训练数据不足:仅 {len(samples)} 条",
            field_name="sample_count",
            invalid_value=len(samples)
        )
    return True

def predict(model_loaded, input_data):
    """模拟模型推理"""
    if not model_loaded:
        raise ModelNotReadyError(
            "请先加载模型再调用推理",
            model_name="GPT-Classifier"
        )
    # 模拟推理结果
    return np.random.random(len(input_data))

# 步骤3:捕获自定义异常
print("【自定义异常实战演示】\n")

# 场景1:数据不足
datasets = [
    ([1, 2, 3, 4, 5], "小数据集(5条)"),     # 不足10条
    ([1]*15, "正常数据集(15条)"),            # 够用
]

for data, desc in datasets:
    print(f"检查 {desc}:", end="")
    try:
        validate_training_data(data)
        print(f"✅ 通过验证,共 {len(data)} 条数据")
    except DataValidationError as e:
        print(f"❌ {e}")
        print(f"   出错字段:{e.field_name},问题值:{e.invalid_value}")

# 场景2:模型未加载
print(f"\n模拟推理(模型未加载):", end="")
try:
    predict(model_loaded=False, input_data=[1, 2, 3])
except ModelNotReadyError as e:
    print(f"❌ {e}(模型名:{e.model_name})")

# 场景3:正常推理
print(f"模拟推理(模型已加载):", end="")
try:
    result = predict(model_loaded=True, input_data=[1, 2, 3, 4, 5])
    print(f"✅ 推理结果:{np.round(result, 3)}")
except ModelNotReadyError as e:
    print(f"❌ {e}")

什么用:在 AI 项目中,自定义异常是实现"优雅降级"的关键。比如 API 服务中,ModelTimeoutError 被捕获后可以返回缓存的结果,DataQualityError 被捕获后可以自动触发数据重新清洗流程。自定义异常让自动化运维成为可能。

三、异常的层次结构——建立"异常家族树"

是什么:自定义异常可以形成继承层次。定义一个基类异常(如 AIProjectError),然后派生多个子类异常(DataErrorModelErrorConfigError)。在 except 中捕获基类即可一次性处理所有子类异常。

大白话 就像动物分类——Animal(基类)下面有 Mammal(哺乳动物)和 Bird(鸟类),Mammal 下面又有 DogCat。如果你想处理所有动物,就捕获 Animal;如果只关心猫,就捕获 Cat。灵活精确!

为什么:在大型 AI 项目中,异常层次结构让错误处理既精细简洁。顶层调用方只需 except AIProjectError 就能捕获所有项目错误并统一记录日志,而内部函数可以 except DataError 做专门的数据修复。分层捕获 = 分层处理。

怎么做

import numpy as np

# ===== 构建异常层次结构 =====

# 第一层:项目基类异常
class AITrainingError(Exception):
    """AI 训练相关异常的基类"""
    def __init__(self, message, error_code=0):
        super().__init__(message)
        self.error_code = error_code

# 第二层:数据相关异常
class DataError(AITrainingError):
    """数据层面的异常"""
    def __init__(self, message, error_code=1000):
        super().__init__(message, error_code)

class DataInsufficientError(DataError):
    """数据量不足"""
    def __init__(self, message, current=0, required=0):
        super().__init__(message, error_code=1001)
        self.current = current
        self.required = required

class DataCorruptionError(DataError):
    """数据损坏"""
    def __init__(self, message):
        super().__init__(message, error_code=1002)

# 第二层:模型相关异常
class ModelError(AITrainingError):
    """模型层面的异常"""
    def __init__(self, message, error_code=2000):
        super().__init__(message, error_code)

class OverfittingWarning(ModelError):
    """过拟合警告"""
    def __init__(self, train_acc=0, val_acc=0):
        super().__init__(
            f"疑似过拟合:训练准确率 {train_acc:.2%} vs 验证准确率 {val_acc:.2%}",
            error_code=2001
        )

class ConvergenceError(ModelError):
    """模型不收敛"""
    def __init__(self, message):
        super().__init__(message, error_code=2002)

# ===== 异常层次的实际使用 =====

def simulate_training(sample_count, train_acc, val_acc):
    """模拟训练流程,展示分层异常处理"""
    
    # 检查数据量
    if sample_count < 100:
        raise DataInsufficientError(
            f"训练数据仅 {sample_count} 条",
            current=sample_count,
            required=100
        )
    
    # 检查过拟合
    if train_acc - val_acc > 0.15:
        raise OverfittingWarning(train_acc=train_acc, val_acc=val_acc)
    
    return "训练正常完成"

# 测试不同场景
print("【异常层次结构实战】\n")
scenarios = [
    {"sample_count": 50, "train_acc": 0.95, "val_acc": 0.80},   # 数据不足
    {"sample_count": 200, "train_acc": 0.98, "val_acc": 0.75},  # 过拟合
    {"sample_count": 200, "train_acc": 0.88, "val_acc": 0.85},  # 正常
]

for i, params in enumerate(scenarios, 1):
    print(f"场景{i}:样本数={params['sample_count']}, train_acc={params['train_acc']}, val_acc={params['val_acc']}")
    try:
        result = simulate_training(**params)
        print(f"  ✅ {result}")
    except DataInsufficientError as e:
        print(f"  ❌ 数据不足异常(错误码 {e.error_code}):{e}")
        print(f"     当前:{e.current},要求:{e.required}")
    except OverfittingWarning as e:
        print(f"  ⚠️  过拟合警告(错误码 {e.error_code}):{e}")
    except AITrainingError as e:
        # 基类兜底:捕获所有项目异常
        print(f"  ❌ 训练异常(错误码 {e.error_code}):{e}")
    print()

# ===== 层次结构的优势 =====
print("【分层捕获的优势】")
advantages = np.array([
    "顶层代码:except AITrainingError 捕获所有项目错误,统一记日志",
    "中间层代码:except DataError 只处理数据问题,做数据修复",
    "底层代码:except DataInsufficientError 精确定位,给出修复建议",
])
for i, adv in enumerate(advantages, 1):
    print(f"  {i}. {adv}")

什么用:在真实的 AI 平台中,异常层次结构是微服务架构的基础。API 网关捕获 AITrainingError 统一返回用户友好的错误信息;调度器捕获 ResourceExhaustedError 自动切换节点;监控系统根据错误码(error_code)统计各类问题的发生频率。分层异常 = 分层运维。

四、raise——主动抛出异常的"警报器"

是什么raise 关键字用于主动抛出异常。除了 raise SomeError('message'),还可以 raise(重新抛出当前异常)和 raise SomeError from original_error(异常链,保留原始异常信息)。

大白话 raise 就像是火灾报警器——不是你等着火烧过来(等 Python 报错),而是发现烟雾就主动拉响警报(主动 raise)。你可以在检测到任何不合规的情况时主动抛出异常,而不是等它自然崩溃。

为什么:有些错误 Python 不会自动检测——比如"用户年龄不能为负数"(-5 是合法的 int),"模型加载后必须调用 warmup() 才能推理"。这些业务约束需要开发者用 raise 主动检查并抛出。raise ... from ... 还可以保留原始异常信息,方便调试。

怎么做

import numpy as np

# ===== raise 的三种用法演示 =====

class ModelConfigError(Exception):
    """模型配置错误"""
    pass

class InferenceError(Exception):
    """推理错误"""
    pass

# 用法1:直接 raise 新异常
print("【raise 三种用法】\n")

def load_model_config(config_dict):
    """加载模型配置,主动检查关键参数"""
    required_keys = ['model_type', 'input_dim', 'output_dim']
    missing = [k for k in required_keys if k not in config_dict]
    
    if missing:
        # 主动抛出异常:配置不完整
        raise ModelConfigError(f"缺少必要配置项:{missing}")
    
    return f"模型配置加载成功:{config_dict['model_type']}"

# 测试
configs = [
    {'model_type': 'CNN', 'input_dim': 784, 'output_dim': 10},  # 完整
    {'model_type': 'RNN', 'input_dim': 256},                     # 缺少 output_dim
]

for cfg in configs:
    try:
        msg = load_model_config(cfg)
        print(f"  ✅ {msg}")
    except ModelConfigError as e:
        print(f"  ❌ {e}")

# 用法2:raise from —— 异常链
print("\n用法2:raise ... from ... 保留原始异常")
try:
    # 尝试将字符串转为 numpy 数组(模拟数据转换)
    raw_input = "这不是数字"
    np.array([float(raw_input)])
except ValueError as original_error:
    try:
        # 包装为更语义化的异常,同时保留原始错误信息
        raise InferenceError("推理输入数据格式错误") from original_error
    except InferenceError as e:
        print(f"  包装后异常:{e}")
        print(f"  原始异常:{e.__cause__}")

# 用法3:裸 raise —— 重新抛出当前异常
print("\n用法3:裸 raise 重新抛出")
def log_and_reraise():
    """记录日志后重新抛出异常,让上层处理"""
    try:
        raise ModelConfigError("临时错误,需要上层决策")
    except ModelConfigError as e:
        print(f"  记录日志:{e}")
        print(f"  重新抛出,让上层决定如何处理...")
        raise  # 不做处理,原样抛出

try:
    log_and_reraise()
except ModelConfigError as e:
    print(f"  上层捕获到:{e}")

什么用:在 AI 系统的 API 层,raise ... from ... 特别有用——底层的 ValueError(如 JSON 解析错误)可以被包装为 BadRequestError 并保留原始堆栈,方便前后端联调。raise 后做清理再重新抛出也是常用模式。

概念关系图谱

概念核心含义与AI的关系关联概念
自定义异常继承 Exception 创建项目专用错误类型模型未加载、数据不足、过拟合等语义化错误Exceptionraise
raise主动抛出异常业务约束检查(数据验证、前置条件)except、异常链
异常层次基类→子类的继承树分层错误处理(数据层/模型层/服务层)继承、多态
raise from异常链,保留原始错误API 层包装底层异常为业务异常异常链、调试
错误码异常携带的数字编码监控系统统计、自动化处理决策error_code、监控
业务约束代码中主动检查的条件训练数据量下限、模型输入维度验证数据验证、前置条件

重点答疑

Q1:自定义异常应该继承 Exception 还是 BaseException

永远继承 ExceptionBaseException 是 Python 所有异常的根类,包括系统级异常(KeyboardInterruptSystemExit 等)。如果继承 BaseException,你的异常在 except Exception 中捕获不到,会破坏常规异常处理逻辑。

Q2:自定义异常需要实现哪些方法?最少要写什么?

最简写法:class MyError(Exception): pass 即可,什么都不用写。如果需要携带额外信息,实现 __init__ 方法(记得调用 super().__init__(message))。__str__ 方法可选——如果没写,Python 会用 Exception 默认的字符串表示。

Q3:什么时候用自定义异常,什么时候用内置异常就够了?

如果内置异常(ValueErrorTypeError 等)能准确表达问题,就用内置的。如果问题需要表达项目特定的语义(如 ModelNotLoadedErrorInsufficientDataError),或者需要在 except区分不同业务错误来做不同处理,就应该自定义。简单原则:能让人一眼看出问题所在的异常,就是好异常。

章节单词汇总

英文音标术语/释义
exception/ɪkˈsepʃn/异常;程序运行时的错误对象
inherit/ɪnˈherɪt/继承;子类获取父类的属性和方法
raise/reɪz/抛出;主动触发异常
hierarchy/ˈhaɪərɑːrki/层次结构;异常类的父子继承关系
semantic/sɪˈmæntɪk/语义;异常类型名称所表达的业务含义
override/ˌoʊvərˈraɪd/重写;子类重新定义父类的方法
constraint/kənˈstreɪnt/约束;代码中对数据或状态的前置条件
propagate/ˈprɑːpəɡeɪt/传播;异常沿着调用栈向上传递

面试练习

Q1 [单选] 创建自定义异常的正确写法是?

  • A. class MyError: pass
  • B. class MyError(Exception): pass
  • C. class MyError(Error): pass
  • D. class MyError(BaseException): pass
解答:自定义异常应该继承 Exception。A 没有继承任何类,不是异常;C 的 Error 不是 Python 内置类;D 继承 BaseException 范围过大,不推荐。

Q2 [单选] raise 关键字的正确用法是?

  • A. raise "出错了"
  • B. raise Exception("出错了")
  • C. throw Exception("出错了")
  • D. raise("出错了")
解答:raise 后面跟异常实例(如 Exception("msg"))或异常类(如 raise ValueError)。A 错误(不能 raise 字符串,Python 3 已废弃),C 是 Java 语法,D 语法错误。

Q3 [单选] raise SomeError("msg") from original_errorfrom 的作用是?

  • A. 忽略原始异常
  • B. 建立异常链,保留原始异常信息
  • C. 把原始异常转换为新异常
  • D. 同时抛出两个异常
解答:raise ... from ... 建立异常链,新异常的 __cause__ 属性指向原始异常,方便在 traceback 中看到完整的错误传导路径。

Q4 [单选] 以下代码中,哪个 except 块会被执行?

class AppError(Exception): pass
class DataError(AppError): pass
try:
    raise DataError()
except AppError:
    print("A")
except DataError:
    print("B")
  • A. A
  • B. B
  • C. A 和 B 都执行
  • D. 都不执行
解答:因为 DataErrorAppError 的子类,所以 except AppError 能匹配到它。except 按顺序匹配,第一个匹配到的就执行,后续不再匹配。应该把 DataError 放前面。

Q5 [单选] 自定义异常中可以添加什么?

  • A. 只能添加错误消息字符串
  • B. 可以添加任意属性(错误码、上下文数据等)和方法
  • C. 只能添加 __init__ 方法
  • D. 只能添加整数错误码
解答:自定义异常是普通 Python 类,可以拥有任意属性(如 error_codetimestampcontext_data)和方法。这是它比内置异常更强大的关键原因。

Q6 [多选] 哪些场景适合创建自定义异常?

  • A. 模型未加载时调用推理
  • B. 训练数据格式不符合预期
  • C. 5 / 0 除零错误
  • D. API 请求频率超过限制
解答:C 是 ZeroDivisionError 内置异常即可表达。ABD 都是业务逻辑层面的错误,内置异常无法精确描述,适合自定义(如 ModelNotLoadedErrorDataFormatErrorRateLimitExceededError)。

Q7 [单选] except 捕获基类异常时,子类异常是否也会被捕获?

  • A. 不会,只能捕获精确匹配的类型
  • B. 会,isinstance 检查匹配
  • C. 只有在多重继承时才会
  • D. 取决于 Python 版本
解答:except 使用 isinstance() 检查匹配,所以捕获基类时会同时捕获所有子类异常。这就是异常层次结构的价值——可以在不同层级做不同粒度的捕获。

Q8 [单选] Python 3 中使用裸 raise(不带任何参数)的效果是?

  • A. 抛出新的 RuntimeError
  • B. 重新抛出当前被捕获的异常
  • C. 什么都不做
  • D. 语法错误
解答:raise 只能在 except 块中使用,作用是重新抛出当前正在处理的异常,保留原始 traceback。常用于"记录日志后让上层处理"的场景。

Q9 [单选] 自定义异常类如果不写 __str__ 方法会怎样?

  • A. 抛出 AttributeError
  • B. 使用 Exception 的默认字符串表示
  • C. 返回空字符串
  • D. 返回类名
解答:Exception 基类已经实现了 __str__,会返回构造函数传入的消息字符串。所以 class MyError(Exception): pass 后用 raise MyError("test") 打印出来就是 "test"。

Q10 [多选] 关于异常处理,以下哪些是良好实践?

  • A. 自定义异常继承自 Exception 而非 BaseException
  • B. 用 raise ... from ... 保留异常链
  • C. 自定义异常携带错误码方便监控
  • D. 在 except 中写出具体异常类型而非裸 except:
解答:全部正确。A 是异常继承规范;B 方便调试;C 便于运维监控;D 避免捕获系统级异常。