错误处理 (Exception Handling)
在程序运行过程中,错误是不可避免的。Python提供了强大的异常处理机制,让我们能够优雅地捕获和处理错误,避免程序崩溃,同时提供有用的错误信息。
什么是异常?
异常(Exception)是程序在执行过程中发生的特殊事件,通常表示出现了错误。当异常发生时,程序会停止执行并创建一个异常对象。
┌─────────────────────────────────────────────────────────────────┐
│ 正常执行 vs 异常处理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【没有异常处理】 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 执行 line 1 │ │
│ │ 执行 line 2 │ │
│ │ 执行 line 3 ← 发生 ZeroDivisionError! │ │
│ │ 程序崩溃,停止执行 │ │
│ │ line 4, line 5 永远不会执行 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 【有异常处理】 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ try: │ │
│ │ 执行 line 1 │ │
│ │ 执行 line 2 │ │
│ │ 执行 line 3 ← 发生异常! │ │
│ │ 跳到 except 块 │ │
│ │ except: │ │
│ │ 处理异常 │ │
│ │ 继续执行 line 4, line 5 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
常见异常类型
| 异常类型 | 说明 | 示例 |
|---|---|---|
ZeroDivisionError |
除数为零 | 10 / 0 |
ValueError |
值错误 | int("abc") |
TypeError |
类型错误 | "2" + 3 |
IndexError |
索引越界 | [1,2,3][10] |
KeyError |
键不存在 | {"a":1}["b"] |
FileNotFoundError |
文件不存在 | open("xxx.txt") |
AttributeError |
属性不存在 | None.method() |
try-except 捕获异常
使用 try-except 语句捕获可能发生异常的代码,并进行处理。
1. 基本语法
Python
try:
# 可能发生异常的代码
result = 10 / 0
print(result)
except ZeroDivisionError:
# 捕获 ZeroDivisionError 异常
print("错误:不能除以零!")
print("程序继续执行...")
2. 捕获异常并获取信息
Python
try:
num = int("abc")
except ValueError as e:
# e 是异常对象,包含错误信息
print(f"发生错误: {e}")
print(f"异常类型: {type(e).__name}")
# 输出:
# 发生错误: invalid literal for int() with base 10: 'abc'
# 异常类型: ValueError
3. 捕获多个异常
Python
# 方式1: 多个 except 子句(按顺序匹配)
try:
result = int("abc")
except ZeroDivisionError:
print("除数为零")
except ValueError:
print("值错误")
except TypeError:
print("类型错误")
# 方式2: 一个 except 捕获多种异常(元组)
try:
result = 10 / "2"
except (ZeroDivisionError, TypeError, ValueError) as e:
print(f"发生错误: {type(e).__name} - {e}")
4. 捕获所有异常
Python
# 方式1: 捕获所有异常(不推荐,可能隐藏真正问题)
try:
# 任何可能出错的代码
pass
except Exception as e:
print(f"发生错误: {e}")
# 方式2: 捕获所有异常(包括系统异常)
try:
# 任何可能出错的代码
pass
except BaseException as e:
print(f"发生严重错误: {e}")
# 方式3: 空 except(最不推荐)
try:
# 任何可能出错的代码
pass
except:
print("出错了")
最佳实践
1. 尽量具体指定异常类型,而不是捕获所有异常
2. 避免空的
3. 记录错误日志而不是仅仅打印
4. 考虑是否真的需要捕获异常,还是应该让程序失败并修复代码
1. 尽量具体指定异常类型,而不是捕获所有异常
2. 避免空的
except: 子句,它会隐藏所有错误3. 记录错误日志而不是仅仅打印
4. 考虑是否真的需要捕获异常,还是应该让程序失败并修复代码
else 和 finally 子句
┌─────────────────────────────────────────────────────────────────┐
│ try-except-else-finally 结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ try: │
│ # 尝试执行的代码 │
│ # 可能抛出异常 │
│ except Exception as e: │
│ # 捕获异常并处理 │
│ else: │
│ # try 块没有发生异常时执行 │
│ finally: │
│ # 无论是否发生异常,始终执行 │
│ # 清理资源 │
│ │
│ 执行顺序: │
│ 1. try 块正常执行 → else 块 → finally │
│ 2. try 块异常 → except 块 → finally │
│ │
└─────────────────────────────────────────────────────────────────┘
Python
try:
num = int("123")
result = num * 2
except ValueError as e:
print(f"转换失败: {e}")
else:
# 只有当 try 块没有发生异常时才会执行
print(f"计算结果: {result}")
finally:
# 无论是否发生异常,始终执行
print("清理资源...")
# 输出:
# 计算结果: 246
# 清理资源...
finally 的典型应用
Python
# 文件操作 - 确保资源释放
# 方式1: 手动关闭(容易遗漏)
f = open("test.txt", "r")
try:
content = f.read()
finally:
f.close() # 确保关闭
# 方式2: with 语句(更简洁,推荐)
with open("test.txt", "r") as f:
content = f.read()
# f 会自动关闭
# 数据库连接 - 确保连接释放
# conn = get_db_connection()
# try:
# result = conn.execute("SELECT * FROM users")
# finally:
# conn.close() # 确保连接关闭
raise 抛出异常
使用 raise 语句主动抛出异常。
1. 基本用法
Python
def divide(a, b):
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(e) # 除数不能为零
2. raise 异常类
Python
# 直接抛出异常类(会自动创建实例)
raise ValueError("值错误信息")
# 抛出异常对象
raise ValueError("值错误信息") # 带消息
raise ValueError() # 不带消息
# 重新抛出当前捕获的异常
try:
# 某段可能出错的代码
pass
except Exception as e:
print(f"记录日志: {e}")
raise # 重新抛出,让调用者处理
3. 异常链 (Python 3)
Python
# 显式异常链 - 在抛出新异常时记录原始异常
try:
int("abc")
except ValueError as e:
raise RuntimeError("转换失败") from e
# 隐式异常链 - raise 语句自动携带当前异常
try:
int("abc")
except ValueError:
raise RuntimeError("转换失败") # 隐式链接
# 禁用异常链
try:
int("abc")
except ValueError as e:
raise RuntimeError("转换失败") from None # 断开链接
自定义异常类
通过继承 Exception 创建自己的异常类。
┌─────────────────────────────────────────────────────────────────┐
│ 自定义异常类结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ class MyError(Exception): ← 自定义异常继承 Exception │
│ pass │
│ │
│ class ValidationError(Exception): ← 验证错误 │
│ def __init__(self, field, message): │
│ self.field = field │
│ self.message = message │
│ super().__init__(f"{field}: {message}") │
│ │
│ class PositiveOnlyError(ValueError): ← 继承 ValueError │
│ """只允许正数的异常""" │
│ pass │
│ │
└─────────────────────────────────────────────────────────────────┘
Python
# 基本自定义异常
class MyError(Exception):
pass
raise MyError("这是一个错误")
# 带详细信息的异常
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
raise ValidationError("age", "年龄必须是正数")
# 继承现有异常类
class PositiveOnlyError(ValueError):
"""数值必须为正的异常"""
pass
def process_positive(num):
if num <= 0:
raise PositiveOnlyError(f"期望正数,实际得到 {num}")
return num * 2
try:
process_positive(-5)
except PositiveOnlyError as e:
print(e)
实际应用:业务异常体系
Python
# 业务异常体系
class AppError(Exception):
"""应用程序基础异常"""
def __init__(self, message, code="APP_ERROR"):
self.message = message
self.code = code
super().__init__(f"[{code}] {message}")
class ValidationError(AppError):
"""验证错误"""
def __init__(self, field, message):
super().__init__(f"字段 {field} 验证失败: {message}", "VALIDATION_ERROR")
self.field = field
class AuthenticationError(AppError):
"""认证错误"""
def __init__(self, message="认证失败"):
super().__init__(message, "AUTH_ERROR")
# 使用
def login(username, password):
if username != "admin":
raise AuthenticationError("用户名不存在")
if password != "123":
raise AuthenticationError("密码错误")
return "登录成功"
try:
print(login("admin", "wrong"))
except AuthenticationError as e:
print(f"错误码: {e.code}, 消息: {e.message}")
最佳实践
异常处理原则
1. LBYL (Look Before You Leap) vs EAFP (Easier to Ask Forgiveness than Permission)
2. LBYL: 先检查再行动 -
3. EAFP: 先行动后处理 -
4. Python 社区更推荐 EAFP 风格
1. LBYL (Look Before You Leap) vs EAFP (Easier to Ask Forgiveness than Permission)
2. LBYL: 先检查再行动 -
if key in dict: value = dict[key]3. EAFP: 先行动后处理 -
try: value = dict[key] except KeyError: ...4. Python 社区更推荐 EAFP 风格
Python
# LBYL 风格 (先检查)
def get_user_age_lbyl(users, user_id):
if user_id in users:
return users[user_id]["age"]
return None
# EAFP 风格 (先尝试) - 更 Pythonic
def get_user_age_eafp(users, user_id):
try:
return users[user_id]["age"]
except KeyError:
return None
# 文件操作的最佳实践
# 使用 with 语句自动管理资源
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# 文件自动关闭,无需 finally
# 异常应该足够具体
# 不好: except Exception:
# 好: except ValueError as e:
知识测验
问题1:以下代码的输出是什么?
try:
print("A")
raise ValueError("error")
print("B")
except ValueError:
print("C")
else:
print("D")
finally:
print("E")
问题2:以下哪个是捕获所有异常的最佳方式?
课后练习
练习1:实现安全的除法函数
实现一个安全的除法函数:
- 接受两个参数 a 和 b
- 如果 b 为零,抛出自定义异常 DivisionByZeroError
- 如果 a 或 b 不是数字,抛出 TypeError
- 返回 a / b 的结果
- 调用者正确捕获并处理异常
练习2:用户输入验证
实现用户注册验证:
- 创建 ValidationError 异常类
- 验证用户名(不能为空,长度3-20)
- 验证密码(长度至少8位)
- 验证邮箱格式
- 收集所有验证错误,而不是遇到第一个就停止
练习3:上下文管理器模拟
模拟数据库连接管理:
- 创建 DatabaseConnection 异常
- 使用 try-except-finally 模拟连接和断开
- 在 finally 中确保连接被正确关闭
- 处理可能的连接超时异常
🐍 Python 在线代码编辑器
📝 运行结果