自定义异常
一句话概述
Python 允许开发者通过继承内置异常类来创建自己的异常类型。自定义异常让错误信息更具语义化,能精准描述业务逻辑层面的错误(如"训练数据不足"、"模型未初始化"),提升代码可读性和调试效率。
💡 核心要点:①自定义异常通过class MyError(Exception): pass继承Exception基类创建 ②用raise关键字主动抛出异常 ③自定义异常可携带额外信息(错误码、上下文数据)便于调试 ④在except中像内置异常一样捕获自定义异常
教学与演示
一、为什么需要自定义异常——内置异常不够用了
是什么:Python 内置的异常类型(ValueError、TypeError 等)描述的是语言层面的错误。但当你的程序有业务逻辑层面的错误时(比如"用户年龄不能超过 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),然后派生多个子类异常(DataError、ModelError、ConfigError)。在 except 中捕获基类即可一次性处理所有子类异常。
大白话 就像动物分类——Animal(基类)下面有Mammal(哺乳动物)和Bird(鸟类),Mammal下面又有Dog和Cat。如果你想处理所有动物,就捕获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 创建项目专用错误类型 | 模型未加载、数据不足、过拟合等语义化错误 | Exception、raise |
raise | 主动抛出异常 | 业务约束检查(数据验证、前置条件) | except、异常链 |
| 异常层次 | 基类→子类的继承树 | 分层错误处理(数据层/模型层/服务层) | 继承、多态 |
raise from | 异常链,保留原始错误 | API 层包装底层异常为业务异常 | 异常链、调试 |
| 错误码 | 异常携带的数字编码 | 监控系统统计、自动化处理决策 | error_code、监控 |
| 业务约束 | 代码中主动检查的条件 | 训练数据量下限、模型输入维度验证 | 数据验证、前置条件 |
重点答疑
Q1:自定义异常应该继承 Exception 还是 BaseException?
永远继承 Exception。BaseException 是 Python 所有异常的根类,包括系统级异常(KeyboardInterrupt、SystemExit 等)。如果继承 BaseException,你的异常在 except Exception 中捕获不到,会破坏常规异常处理逻辑。
Q2:自定义异常需要实现哪些方法?最少要写什么?
最简写法:class MyError(Exception): pass 即可,什么都不用写。如果需要携带额外信息,实现 __init__ 方法(记得调用 super().__init__(message))。__str__ 方法可选——如果没写,Python 会用 Exception 默认的字符串表示。
Q3:什么时候用自定义异常,什么时候用内置异常就够了?
如果内置异常(ValueError、TypeError 等)能准确表达问题,就用内置的。如果问题需要表达项目特定的语义(如 ModelNotLoadedError、InsufficientDataError),或者需要在 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_error 中 from 的作用是?
- 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. 都不执行
解答:因为DataError是AppError的子类,所以except AppError能匹配到它。except按顺序匹配,第一个匹配到的就执行,后续不再匹配。应该把DataError放前面。
Q5 [单选] 自定义异常中可以添加什么?
- A. 只能添加错误消息字符串
- B. 可以添加任意属性(错误码、上下文数据等)和方法
- C. 只能添加
__init__方法 - D. 只能添加整数错误码
解答:自定义异常是普通 Python 类,可以拥有任意属性(如error_code、timestamp、context_data)和方法。这是它比内置异常更强大的关键原因。
Q6 [多选] 哪些场景适合创建自定义异常?
- A. 模型未加载时调用推理
- B. 训练数据格式不符合预期
- C.
5 / 0除零错误 - D. API 请求频率超过限制
解答:C 是ZeroDivisionError内置异常即可表达。ABD 都是业务逻辑层面的错误,内置异常无法精确描述,适合自定义(如ModelNotLoadedError、DataFormatError、RateLimitExceededError)。
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 避免捕获系统级异常。