Python 库设计中,何时使用自定义异常或内置异常?
建议混合使用:文件不存在时使用 FileNotFoundError;视频打开、读取和标定等领域失败使用自定义异常。是否需要新增异常类型,主要看调用方是否需要根据类型采取不同的处理措施。
要求调用方引用自定义异常是正常的 API 设计。不要在出口把这些异常统一改成普通 Exception,否则调用方将无法按类型处理错误。
如何为当前场景选择异常
原有的两种方案分别将多种视频问题归入 VideoProcessingError 或 ValueError。前者便于捕获视频相关失败;后者虽然更常见,却可能同时捕获标定算法、参数转换等位置抛出的无关 ValueError。
可以先采用以下约定:
| 情况 | 建议异常 | 设计依据 |
|---|---|---|
| 本地视频文件不存在 | FileNotFoundError | 已有含义明确的内置类型 |
| 参数类型不受支持 | TypeError | 表达调用参数的类型问题 |
| 参数类型正确,但取值不符合约定 | ValueError | 表达参数值问题 |
| 视频无法打开或无法取得所需帧 | VideoProcessingError | 表达视频处理领域的失败 |
| 检测结果不足,无法完成标定 | CalibrationError | 调用方可能需要更换素材或调整标定条件 |
这是针对当前问题的设计建议,Python 并没有强制规定。如果 API 明确将不可用的视频定义为无效输入,也可以使用 ValueError。重要的是保持语义一致,并在文档中说明。
异常类型应根据调用方的处理方式来设计。如果打开失败和读取失败都只需要提示用户换一个视频,一个 VideoProcessingError 就够用。调用方确实需要分别处理时,再增加 VideoOpenError、VideoReadError 等子类。
自定义异常需要怎样的继承关系
可以为库的领域异常提供一个公共父类:
class MylibError(Exception):
pass
class VideoProcessingError(MylibError):
pass
class CalibrationError(MylibError):
pass
调用方可以分别捕获视频失败和标定失败,也可以用 except MylibError 处理这组预期的领域失败。
在这个方案中,FileNotFoundError 不属于 MylibError。如果调用方要统一处理文件缺失和领域失败,可以捕获异常元组:
except (FileNotFoundError, mylib.MylibError):
...
文档中需要说明这一点,避免用户误以为 MylibError 能捕获库执行过程中出现的所有异常。
VideoProcessingError(ValueError) 在语法上可行,捕获 ValueError 时也会捕获它。但继承关系也表达语义上的归属:只有所有 VideoProcessingError 都确实表示无效参数值时,这种关系才合适。解码后端失败或处理过程中的读取失败,未必符合这个含义。
优先采用简单的单继承结构。Python 官方文档建议避免同时继承多个异常类型,因为可能涉及 args 处理冲突和底层内存布局兼容问题。Python 内置异常文档
视频校验可以怎样实现
下例假设输入是本地文件路径,并要求视频至少能提供一帧。示例只展示异常定义和校验部分,run() 与标定算法需要结合库的实际情况实现。
from pathlib import Path
import cv2
class MylibError(Exception):
pass
class VideoProcessingError(MylibError):
pass
class CalibrationError(MylibError):
pass
class Detector:
def _validate_video(self, video_path):
path = Path(video_path)
if not path.exists():
raise FileNotFoundError(f"Video not found: {path}")
cap = None
try:
cap = cv2.VideoCapture(str(path))
if not cap.isOpened():
raise VideoProcessingError(
f"Cannot open video: {path}"
)
ret, frame = cap.read()
if not ret or frame is None:
raise VideoProcessingError(
f"Cannot read first frame: {path}"
)
except cv2.error as exc:
raise VideoProcessingError(
f"Video backend failed: {path}"
) from exc
finally:
if cap is not None:
cap.release()
使用时需要注意以下边界:
exists()只是预检查。检查后文件仍可能被删除或改变,后续打开也可能失败。isOpened()返回失败,不能确定文件已损坏或格式错误。错误消息应描述观察到的失败,避免猜测具体原因。- 首帧读取成功只能说明取得了首帧,不能证明整个视频都能完整读取。
- 持续读取时,
read()返回False也可能表示视频正常结束。应根据是否必须取得这一帧的业务约定,决定是否抛出异常。OpenCV VideoCapture 文档
标定阶段确认检测结果不足时,可以直接抛出 CalibrationError,并在消息中说明实际缺少什么条件。由于输入没有提供标定实现,无法确定具体检测阈值或判定公式。
调用方必须导入所有异常吗
不需要。调用方可以只导入库,通过模块名访问异常:
import mylib
detector = mylib.Detector()
try:
detector.run("video.mp4")
except FileNotFoundError as exc:
print(f"找不到视频文件:{exc}")
except mylib.CalibrationError as exc:
print(f"标定失败:{exc}")
except mylib.VideoProcessingError as exc:
print(f"视频处理失败:{exc}")
这种写法假设库在公开入口导出了这些类。也可以将异常统一放在 mylib.errors 中,让调用方使用 mylib.errors.CalibrationError。
具体异常应放在公共父类之前捕获,否则父类分支会先匹配,后面的细分处理便不会执行。Python 异常处理教程
库需要提供稳定的公开导入路径,并说明每个异常何时抛出。调用方只需了解自己准备处理的类型。
requests、NumPy 和 pandas 怎么做
这三个库都提供自定义异常,调用方无需导入全部类型。
- requests 提供
RequestException,以及Timeout、ConnectionError等子类。调用方可以导入某个类,也可以使用requests.exceptions.Timeout这样的限定名称。Requests 异常定义 - NumPy 提供
numpy.exceptions.AxisError。文档明确说明,为了兼容历史上捕获ValueError或IndexError的代码,它同时继承了这两个类型。这是针对兼容需求的具体设计,新库不必因此普遍采用异常多继承。NumPy AxisError 文档 - pandas 提供
pandas.errors.EmptyDataError等异常。例如,read_csv遇到没有数据或表头的输入时可能抛出该异常。调用方可以通过pd.errors.EmptyDataError引用它。pandas EmptyDataError 文档
自定义异常是常见做法,但各个库不必采用同一套异常层级。
为什么不应该统一改抛 Exception
原先的尝试在捕获 VideoProcessingError 后执行:
raise Exception("Video validation failed.")
这样会改变调用方直接捕获到的异常类型,也会使外层消息失去具体细节。不过,“所有错误信息都丢失”并不准确:在 except 内抛出新异常时,Python 默认会保留原异常作为隐式上下文。
如果无需转换异常,让它直接传播即可。确实需要记录日志时,可以记录后使用裸 raise:
except VideoProcessingError:
logger.exception("Video validation failed")
raise
如果需要把底层异常转换成公开的领域异常,则使用显式异常链:
except cv2.error as exc:
raise VideoProcessingError("Video decoding failed") from exc
raise ... from exc 会保留明确的因果关系;裸 raise 则重新抛出当前异常。Python 异常链教程
建议只在能够恢复、增加必要上下文或转换抽象层次的位置捕获异常。避免把整个 run() 中的任意 Exception 都转换成视频错误,否则程序缺陷也可能被误报为输入问题。日志由库或应用的合适一层负责,以减少重复记录。
公开文档应列出预期异常、触发条件和可供读取的属性。调用方应按异常类型或结构化属性判断,避免解析错误消息来决定处理流程。
备注:内容仅供参考。
