异常处理(Exceptions)
一. 认识异常
异常(Exception)是指在程序运行(执行)期间发生的影响正常执行流程的错误事件。
- 与语法错误(Syntax Errors)不同:语法错误是代码不符合Python的书写规范,在运行前就会被拦截;而异常是语法没问题,但在运行过程中遇到了无法处理的情况(如除以零、文件找不到等)。
- 如果不对异常进行捕获和处理,程序遇到异常时会直接崩溃并终止运行。
- Python中所有的异常本质上都是对象,它们继承自基础的
BaseException类。
二. 常见内置异常类型
在日常开发中,掌握常见的异常类型有助于快速定位和排查问题。
| 异常名称 | 触发场景描述 |
|---|---|
| Exception | 常规异常的基类(兜底异常),包含下面绝大多数常见异常。 |
| ZeroDivisionError | 当除法运算的除数(分母)为 0 时触发。 |
| TypeError | 当操作或函数应用于不适当类型的对象时触发(如字符串和整数相加)。 |
| ValueError | 当内置操作或函数接收到类型正确但值不合法的参数时触发(如 int("abc"))。 |
| IndexError | 当尝试访问序列(列表、元组、字符串)中不存在的索引时触发(越界)。 |
| KeyError | 当尝试访问字典中不存在的键(Key)时触发。 |
| FileNotFoundError | 试图打开一个不存在的文件或目录时触发。 |
| AttributeError | 当尝试访问对象不存在的属性或方法时触发。 |
| NameError | 当尝试访问一个未声明/未初始化的局部或全局变量时触发。 |
| ImportError | 当 import 语句无法找到指定的模块,或者 from ... import 找不到指定名称时触发。 |
三. 异常的捕获与处理 (try-except)
Python使用 try...except... 语句块来捕获和处理异常。它的核心思想是:尝试执行可能出错的代码,如果真的出错了,就执行备用方案,而不是让程序直接死掉。
1. 基础异常捕获
作用:对一段可能发生错误的代码进行监控,一旦发生指定类型的异常,就拦截它并执行 except 块内的代码。
python
try:
# 可能发生异常的代码块
except 异常类型:
# 发生指定异常时执行的代码块
示例:
python
try:
num = 10 / 0
except ZeroDivisionError:
print("错误:除数不能为零!")
# 程序不会崩溃,会继续向下执行
print("程序结束")
2. 获取异常对象的详细信息 (as e)
作用:在捕获异常的同时,将异常对象赋值给一个变量(通常命名为 e 或 err),以便在处理时打印底层的具体报错信息。
python
except 异常类型 as e:
示例:
python
try:
num = int("hello")
except ValueError as e:
print(f"发生值错误,系统提示内容为: {e}")
3. 捕获多种类型的异常
作用:同一段代码可能会抛出不同类型的异常,你可以使用多个 except 块来分别处理,或者将它们放在同一个元组中统一处理。
示例:
python
# 方式一:分开处理(推荐,逻辑更清晰)
try:
a = [1, 2, 3]
print(a[5])
int("xyz")
except IndexError:
print("错误:列表索引越界!")
except ValueError:
print("错误:类型转换失败!")
# 方式二:合并处理
try:
print(10 / 0)
except (ZeroDivisionError, TypeError) as e:
print(f"发生了数学运算错误或类型错误: {e}")
4. 万能/兜底捕获 (Exception)
作用:当你不知道代码具体会抛出什么异常,或者想在最后拦截所有未预料到的错误时,可以使用常规异常的基类 Exception 来兜底。
注:通常建议把它放在所有具体异常的最后面,作为最后一道防线。
示例:
python
try:
# 复杂的业务逻辑
result = 100 / int(input("请输入一个数字: "))
except ValueError:
print("必须输入纯数字!")
except ZeroDivisionError:
print("输入的数字不能是0!")
except Exception as e:
# 兜底捕获其他所有未知异常
print(f"发生未知错误,请联系管理员: {e}")
四. 完整的异常处理结构
除了 try 和 except,Python 还提供了 else 和 finally 两个可选的关键字,用于构建更严谨的代码逻辑。
python
try:
# 主体业务代码 (必须有)
except Exception:
# 发生异常时执行 (必须有至少一个except)
else:
# 未发生异常时执行 (可选)
finally:
# 无论是否发生异常,最终一定会执行 (可选)
1. else 分支 - 一切顺利时执行
作用:当 try 块中的代码完全没有抛出任何异常时,才会执行 else 块中的代码。
示例:
python
try:
res = 10 / 2
except ZeroDivisionError:
print("出错了")
else:
print(f"计算成功!结果是: {res}") # 如果没报错,就会执行这句
2. finally 分支 - 无论如何都要执行
作用:无论 try 块内是否发生异常,或者异常是否被成功捕获,finally 块中的代码最终都一定会执行。极其常用于释放系统资源(如:关闭文件、关闭数据库连接、释放锁等)。
示例:
python
f = None
try:
f = open('test.txt', 'r', encoding='utf-8')
content = f.read()
except FileNotFoundError:
print("文件不存在!")
finally:
# 哪怕上面读取报错了,这里也一定会执行,确保文件被安全关闭
if f:
f.close()
print("文件已自动关闭。")
五. 主动抛出异常 (raise)
作用:有时系统并没有报错,但在业务逻辑上这是不允许的(比如用户输入的年龄为负数),此时你可以使用 raise 关键字主动中断程序并抛出一个异常。
python
raise 异常类型("自定义的错误提示信息")
示例:
python
def check_age(age):
if age < 0 or age > 150:
# 主动触发 ValueError 异常
raise ValueError(f"不合理的年龄值: {age}")
return True
try:
check_age(-5)
except ValueError as e:
print(f"拦截到业务异常: {e}")
六. 自定义异常 (Custom Exceptions)
作用:Python内置的异常通常针对通用场景。在复杂的项目中,为了区分底层的系统错误和上层的业务逻辑错误,通常需要创建自己专属的异常类型,让代码的报错信息更具备可读性。
注:自定义异常类必须直接或间接继承自 Exception 类!
1. 自定义异常需要重写的方法
严格来说,如果你只需要一个用来占位的"异常名字",类里面直接写 pass 就可以。但为了让异常能携带业务数据并输出友好的提示,通常需要重写以下两个核心魔术方法:
__init__(self, ...)(初始化方法): 用于接收并保存触发异常时的关键上下文数据(如报错时的具体数值、错误码等)。如果你的异常类没有极其特殊的处理,重写时记得调用super().__init__()初始化父类,或者直接保存属性供后续打印使用。__str__(self)(字符串表示方法): 用于定制当异常被捕获并打印时(如print(e)),终端输出的具体字符串格式。重写这个方法可以让你的错误提示更加直观、专业。
2. 示例
python
# 1. 定义一个继承自 Exception 的自定义异常类
class InsufficientFundsError(Exception):
# 重写 __init__,接收引发异常时的业务数据
def __init__(self, balance, amount):
self.balance = balance # 当前余额
self.amount = amount # 试图扣除的金额
# 注意:这里也可以选择调用 super().__init__(...) 交给父类处理,
# 但为了更灵活地控制输出,通常推荐保存为实例属性,再重写 __str__。
# 重写 __str__,定义异常被打印时的表现形式
def __str__(self):
return f"余额不足!当前账户余额: {self.balance} 元, 但试图扣款: {self.amount} 元。"
# 2. 在业务逻辑中主动抛出它
def withdraw(balance, amount):
if amount > balance:
# 当逻辑不符时,实例化并 raise 这个自定义异常
raise InsufficientFundsError(balance, amount)
return balance - amount
# 3. 捕获自定义异常
try:
withdraw(100, 500)
except InsufficientFundsError as e:
# 这里的 print(e) 会自动触发 InsufficientFundsError 类中的 __str__ 方法
print(f"支付流程中断 -> {e}")
七. 断言 (assert)
作用:断言用于在开发和测试阶段声明某个条件必须为真(True)。如果条件为假(False),程序会立即引发 AssertionError 异常并终止。
注:断言是一种"防御性编程"工具,主要用于排查开发期的逻辑 Bug,不建议用来处理运行时可能发生的常规用户输入错误。在生产环境中,可以通过命令行参数 -O (大写字母O) 忽略所有 assert 语句。
python
assert 布尔表达式, "可选的错误提示信息"
示例:
python
def apply_discount(price, discount):
new_price = price * discount
# 断言:折后价格绝不可能低于 0,也不可能高于原价
assert 0 <= new_price <= price, "折扣计算逻辑发生致命错误!"
return new_price
# 正常调用
apply_discount(100, 0.8)
# 非法调用,将触发 AssertionError: 折扣计算逻辑发生致命错误!
# apply_discount(100, 1.2)