1、异常处理基础:什么是异常、异常处理的重要性、Python中的异常类层次结构

1.1 到底什么是异常?

异常,说白了就是程序运行时出了岔子。

你写了一段代码,逻辑上看着没问题。但一跑起来,文件找不到、网络断了、内存不够了……这些都属于异常。我习惯把异常理解成「程序在正常流程之外遇到的意外状况」。

举个例子:

# 一个简单的除法
result = 10 / 0
print(result)

这段代码一执行,Python 就会抛出一个 ZeroDivisionError。程序直接崩溃,后面的代码全废了。

为什么会这样?因为分母不能为零。这是数学规则,也是程序必须遵守的底线。

嗯,这里要注意:异常不是 bug,但处理不好就会变成 bug。bug 是你代码写错了,异常是运行时环境给你使绊子。两者有本质区别。

1.2 异常处理的重要性

你想想看,一个线上跑着的自动化脚本,突然因为某个小异常挂了。后果是什么?

  • 任务中断,数据丢失
  • 需要人工介入,半夜爬起来重启
  • 严重时可能导致整个系统雪崩

我在项目中遇到过一件事。有一次我们做日志采集脚本,跑得好好的。结果某天某个日志文件被运维误删了,脚本直接崩溃。那一整天的日志全丢了。后来复盘时发现,就少了一个 try-except 包裹。

所以异常处理的重要性,我总结为三点:

重要性 说明
保证程序不崩溃 遇到异常时优雅降级,而不是直接挂掉
提供有意义的错误信息 让运维人员一眼看出问题出在哪
实现自动化恢复 某些异常可以自动重试或回滚,无需人工干预

核心观点: 没有异常处理的脚本,就像没有安全带的赛车。跑得快,但出事就是大事。

1.3 Python 中的异常类层次结构

Python 的异常体系,其实是一棵继承树。我刚开始学的时候也觉得这东西有点绕,但后来发现,理解了这棵树,写异常处理就顺手多了。

最顶层的基类是 BaseException。它下面有两个主要分支:

  • SystemExitKeyboardInterruptGeneratorExit —— 这些是系统级异常,一般不建议捕获
  • Exception —— 这才是我们日常打交道的大头

Exception 下面又分了很多子类:

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── FloatingPointError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError
    │   ├── FileNotFoundError
    │   └── PermissionError
    ├── ValueError
    ├── TypeError
    └── ... 还有很多

这张表我建议你记一下,写代码时经常用到:

异常类型 触发场景 常见例子
ZeroDivisionError 除数为零 1/0
FileNotFoundError 文件不存在 open('不存在的文件.txt')
ValueError 值不合法 int('abc')
TypeError 类型不匹配 '1' + 2
KeyError 字典键不存在 {'a':1}['b']
IndexError 列表索引越界 [1,2,3][10]

我的习惯: 捕获异常时尽量用具体的子类,而不是笼统地捕获 Exception。比如你知道可能发生 FileNotFoundError,就只捕获它。这样不会把其他意外异常也吞掉。

1.4 一个简单的异常处理示例

光说不练假把式。我们来看一个实际场景:读取配置文件。

def load_config(file_path):
    try:
        with open(file_path, 'r') as f:
            config = f.read()
            return config
    except FileNotFoundError:
        print(f'配置文件 {file_path} 不存在,使用默认配置')
        return 'default_config'
    except PermissionError:
        print(f'没有权限读取 {file_path},请检查文件权限')
        return None
    except Exception as e:
        print(f'读取配置时发生未知错误: {e}')
        return None

这段代码做了三件事:

  1. 正常读取文件
  2. 文件不存在时,降级使用默认配置
  3. 权限不足时,给出明确提示

我曾经踩过的坑: 早期我写异常处理时,喜欢用 except Exception as e: pass。觉得这样程序就不会崩了。结果呢?异常被静默吞掉,问题根本发现不了。后来线上出了故障,查了半天才发现是某个异常被 pass 了。从那以后,我再也不敢随便 pass 异常了。

1.5 异常处理的黄金法则

总结几条我这些年积累下来的经验:

  • 能精确就精确:捕获具体的异常类型,别一把抓
  • 别吞异常:要么处理,要么往上抛,别静默忽略
  • 日志要打全:捕获异常时一定要记录堆栈信息,方便排查
  • 考虑降级方案:异常发生时,程序能不能继续跑?能的话怎么跑?

说白了,异常处理不是为了消灭异常,而是为了在异常发生时,程序还能体面地活着。

好了,这一章就到这里。下一章我们聊聊如何用 try-except-else-finally 构建更健壮的异常处理结构。