异常处理
异常处理是 Python 编程中不可或缺的部分,它让程序能够优雅地处理错误情况,而不是直接崩溃。本章节将详细介绍 Python 的异常处理机制。
异常基础
什么是异常
异常(Exception)是程序在运行过程中发生的错误或异常情况。当异常发生时,如果没有适当的处理,程序会终止并显示错误信息。
# 示例:会发生异常的代码
print(10 / 0) # ZeroDivisionError: division by zero
num = int("abc") # ValueError: invalid literal for int()
my_list = [1, 2, 3]
print(my_list[5]) # IndexError: list index out of range
my_dict = {"name": "张三"}
print(my_dict["age"]) # KeyError: 'age'
异常 vs. 语法错误
- 语法错误(SyntaxError):代码不符合 Python 语法规则,程序无法运行
- 异常(Exception):程序在运行过程中发生的错误,代码语法正确
# 语法错误示例
if True # SyntaxError: expected ':' after 'if' statement
print("Hello")
# 运行时异常示例
result = 10 / 0 # 语法正确,但运行时出错
为什么需要异常处理
没有异常处理的后果:
def divide(a, b):
return a / b
# 用户输入 0 时程序崩溃
result = divide(10, 0) # 程序终止,显示错误信息
print("这行代码不会执行")
使用异常处理的好处:
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None # 返回友好提示或默认值
# 或者: print("除数不能为零!")
result = divide(10, 0)
if result is not None:
print(f"结果: {result}")
else:
print("除数不能为零!")
print("程序继续执行...")
优势总结:
- 防止程序崩溃:优雅处理错误,保持程序运行
- 提供友好提示:向用户提供清晰的错误信息
- 分离错误处理:将正常逻辑与错误处理分离
- 便于调试:记录异常信息,帮助定位问题
try-except
基本语法
try:
# 可能发生异常的代码
risky_code()
except ExceptionType:
# 处理异常的代码
handle_exception()
示例:
try:
num1 = int(input("请输入第一个数字: "))
num2 = int(input("请输入第二个数字: "))
result = num1 / num2
print(f"{num1} ÷ {num2} = {result}")
except ValueError:
print("错误:请输入有效的数字!")
except ZeroDivisionError:
print("错误:除数不能为零!")
执行流程:
用户输入: abc
请输入第一个数字: abc
错误:请输入有效的数字!
用户输入: 10 和 0
请输入第一个数字: 10
请输入第二个数字: 0
错误:除数不能为零!
用户输入: 10 和 2
请输入第一个数字: 10
请输入第二个数字: 2
10 ÷ 2 = 5.0
多个 except
可以使用多个 except 子句来捕获不同类型的异常。
def process_list(items, index, divisor):
try:
result = items[index] / divisor
return result
except IndexError:
print("错误:索引超出列表范围!")
except ZeroDivisionError:
print("错误:除数不能为零!")
except TypeError:
print("错误:类型不匹配,无法进行除法运算!")
# 测试不同异常
process_list([1, 2, 3], 5, 2) # IndexError
process_list([1, 2, 3], 1, 0) # ZeroDivisionError
process_list([1, 2, 3], 1, "2") # TypeError
process_list([1, 2, 3], 1, 2) # 正常执行,返回 1.0
捕获多个异常类型
可以使用元组在一个 except 中捕获多个异常类型。
try:
num = int(input("请输入数字: "))
result = 100 / num
print(f"结果: {result}")
except (ValueError, ZeroDivisionError) as e:
print(f"发生错误: {type(e).__name__}")
print(f"错误信息: {e}")
# 输入 "abc" -> ValueError: invalid literal for int() with base 10: 'abc'
# 输入 "0" -> ZeroDivisionError: division by zero
捕获所有异常
不推荐的做法(捕获所有异常):
try:
# 某些代码
result = risky_operation()
except: # 裸 except,不推荐
print("发生了错误")
稍好的做法:
try:
result = risky_operation()
except Exception as e: # 捕获所有 Exception(不捕获 SystemExit 等)
print(f"发生错误: {e}")
最佳实践:
try:
result = risky_operation()
except ValueError as e:
print(f"值错误: {e}")
except TypeError as e:
print(f"类型错误: {e}")
except Exception as e: # 最后的兜底处理
print(f"未预期的错误: {e}")
import traceback
traceback.print_exc() # 打印完整堆栈跟踪
注意:
- 避免使用裸
except:(不带异常类型),它会捕获所有异常包括 KeyboardInterrupt 和 SystemExit - 尽量捕获具体的异常类型,而不是使用通用的 Exception
- 兜底的 Exception 捕获应该放在最后的 except 子句中
else 子句
else 子句在 try 块没有发生异常时执行。
def divide_and_print(a, b):
try:
result = a / b
except ZeroDivisionError:
print("除数不能为零!")
else:
# 只有 try 块成功执行时才会运行
print(f"计算结果: {result}")
print("计算完成!")
divide_and_print(10, 2)
# 输出:
# 计算结果: 5.0
# 计算完成!
divide_and_print(10, 0)
# 输出:
# 除数不能为零!
使用场景:
def read_and_process_file(filename):
try:
file = open(filename, 'r')
content = file.read()
except FileNotFoundError:
print(f"错误:文件 {filename} 不存在!")
except IOError:
print(f"错误:读取文件 {filename} 失败!")
else:
# 文件成功读取后才执行处理逻辑
print(f"文件内容长度: {len(content)} 字符")
processed = content.upper() # 处理内容
print("处理完成")
file.close()
return processed
# 使用
result = read_and_process_file("example.txt")
else 的优势:
- 逻辑清晰:将正常处理逻辑与错误处理分离
- 避免意外捕获:else 中的代码不会捕获 try 块中的异常
- 代码可读性:明确表示"成功后才执行的逻辑"
finally 子句
finally 子句无论是否发生异常都会执行,通常用于清理资源。
def divide_with_finally(a, b):
try:
result = a / b
print(f"计算结果: {result}")
return result
except ZeroDivisionError:
print("除数不能为零!")
return None
finally:
# 无论是否发生异常都会执行
print("清理资源...")
print("--- 测试 1 ---")
divide_with_finally(10, 2)
# 输出:
# 计算结果: 5.0
# 清理资源...
print("\n--- 测试 2 ---")
divide_with_finally(10, 0)
# 输出:
# 除数不能为零!
# 清理资源...
完整语法(try-except-else-finally):
def process_data(data):
try:
# 尝试执行的操作
result = int(data)
except ValueError as e:
# 发生异常时的处理
print(f"转换失败: {e}")
result = None
else:
# 没有异常时执行
print(f"转换成功: {result}")
result = result * 2
finally:
# 无论是否异常都执行
print("处理完成\n")
return result
# 测试
process_data("10") # 成功
process_data("abc") # 失败
# 输出:
# 转换成功: 10
# 处理完成
#
# 转换失败: invalid literal for int() with base 10: 'abc'
# 处理完成
finally 的典型应用:资源清理
def read_file_safely(filename):
file = None
try:
file = open(filename, 'r')
content = file.read()
return content
except FileNotFoundError:
print(f"文件 {filename} 不存在")
return None
except IOError as e:
print(f"读取文件出错: {e}")
return None
finally:
# 确保文件总是被关闭
if file is not None:
file.close()
print("文件已关闭")
# 更好的做法:使用 with 语句(上下文管理器)
def read_file_with_context(filename):
try:
with open(filename, 'r') as file:
return file.read()
except FileNotFoundError:
print(f"文件 {filename} 不存在")
return None
# with 语句会自动关闭文件,无需手动清理
finally 执行时机:
def test_finally_timing():
try:
print("try 块开始")
return "try 返回值"
except:
return "except 返回值"
finally:
print("finally 块执行")
# finally 中的 return 会覆盖 try/except 的 return
return "finally 返回值"
result = test_finally_timing()
print(f"最终返回值: {result}")
# 输出:
# try 块开始
# finally 块执行
# 最终返回值: finally 返回值
注意: finally 中的 return 语句会覆盖 try 或 except 中的 return,因此尽量避免在 finally 中使用 return。
异常对象
可以使用 as 关键字捕获异常对象,获取详细的错误信息。
try:
num = int("abc123")
except ValueError as e:
print(f"异常类型: {type(e)}")
print(f"异常名称: {type(e).__name__}")
print(f"异常信息: {e}")
print(f"异常字符串表示: {str(e)}")
# 输出:
# 异常类型: <class 'ValueError'>
# 异常名称: ValueError
# 异常信息: invalid literal for int() with base 10: 'abc123'
# 异常字符串表示: invalid literal for int() with base 10: 'abc123'
使用异常对象进行详细处理:
def parse_user_input(input_str):
try:
# 尝试解析为整数
num = int(input_str)
return num, "整数"
except ValueError as e:
# 如果不是整数,尝试解析为浮点数
try:
num = float(input_str)
return num, "浮点数"
except ValueError:
# 都不是,返回原始字符串
return input_str, "字符串"
# 测试
print(parse_user_input("42")) # (42, '整数')
print(parse_user_input("3.14")) # (3.14, '浮点数')
print(parse_user_input("hello")) # ('hello', '字符串')
常见异常类型
Python 有许多内置异常类型,以下是最常见的几种:
基础异常类型
# 1. AttributeError - 属性不存在
class Person:
def __init__(self, name):
self.name = name
person = Person("张三")
print(person.age) # AttributeError: 'Person' object has no attribute 'age'
# 2. IndexError - 索引超出范围
my_list = [1, 2, 3]
print(my_list[5]) # IndexError: list index out of range
# 3. KeyError - 键不存在
my_dict = {"name": "李四", "age": 25}
print(my_dict["address"]) # KeyError: 'address'
# 安全访问字典键的方式
print(my_dict.get("address", "默认值")) # 输出: 默认值
# 4. NameError - 变量未定义
print(undefined_variable) # NameError: name 'undefined_variable' is not defined
# 5. TypeError - 类型错误
result = "10" + 5 # TypeError: can only concatenate str (not "int") to str
result = "10" + str(5) # 正确: "105"
# 6. ValueError - 值错误
num = int("abc") # ValueError: invalid literal for int() with base 10: 'abc'
num = int("10.5") # ValueError: invalid literal for int() with base 10: '10.5'
num = float("10.5") # 正确
# 7. ZeroDivisionError - 除数为零
result = 10 / 0 # ZeroDivisionError: division by zero
# 8. FileNotFoundError - 文件不存在
file = open("nonexistent.txt") # FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.txt'
# 9. PermissionError - 权限不足
# file = open("/root/protected.txt") # PermissionError: [Errno 13] Permission denied
# 10. ImportError / ModuleNotFoundError - 导入错误
import nonexistent_module # ModuleNotFoundError: No module named 'nonexistent_module'
# 11. OverflowError - 数值溢出
import math
result = math.exp(1000) # OverflowError: math range error
# 12. RecursionError - 递归过深
def infinite_recursion():
infinite_recursion()
# infinite_recursion() # RecursionError: maximum recursion depth exceeded
异常层次结构
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
├── TypeError
├── ValueError
├── RuntimeError
├── AttributeError
├── ImportError
└── ...
利用异常层次结构:
def handle_errors():
try:
# 某些操作
items = [1, 2, 3]
index = 5
value = items[index]
except LookupError as e: # 捕获 IndexError 和 KeyError
print(f"查找错误: {e}")
except Exception as e: # 捕获所有其他异常
print(f"其他错误: {e}")
handle_errors() # 输出: 查找错误: list index out of range
常见异常处理示例
字典操作:
# 不使用异常处理
my_dict = {"name": "张三"}
if "age" in my_dict:
age = my_dict["age"]
else:
age = 0
# 使用异常处理
try:
age = my_dict["age"]
except KeyError:
age = 0
# 最优雅的方式
age = my_dict.get("age", 0)
列表操作:
def safe_get_item(items, index, default=None):
"""安全获取列表元素"""
try:
return items[index]
except (IndexError, TypeError):
return default
print(safe_get_item([1, 2, 3], 1)) # 2
print(safe_get_item([1, 2, 3], 5)) # None
print(safe_get_item(None, 0)) # None
print(safe_get_item([1, 2, 3], 5, 0)) # 0
数值转换:
def safe_int_convert(value, default=0):
"""安全地将值转换为整数"""
try:
return int(value)
except (ValueError, TypeError):
return default
print(safe_int_convert("123")) # 123
print(safe_int_convert("abc")) # 0
print(safe_int_convert(None)) # 0
print(safe_int_convert("3.14")) # 0
print(safe_int_convert("3.14", 3)) # 3
抛出异常
raise 语句
使用 raise 语句可以手动抛出异常。
# 抛出内置异常
def divide(a, b):
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
try:
result = divide(10, 0)
except ZeroDivisionError as e:
print(f"捕获到异常: {e}")
# 输出: 捕获到异常: 除数不能为零
抛出异常的不同方式:
# 方式 1: 抛出异常实例
raise ValueError("值无效")
# 方式 2: 重新抛出捕获的异常
try:
risky_operation()
except Exception as e:
# 进行一些日志记录
print(f"记录异常: {e}")
raise # 重新抛出原始异常
# 方式 3: 抛出新的异常并保留原始异常信息
try:
risky_operation()
except Exception as original_error:
raise ValueError("处理失败") from original_error
条件抛出异常
def set_age(age):
"""设置年龄,验证参数有效性"""
if not isinstance(age, int):
raise TypeError("年龄必须是整数")
if age < 0:
raise ValueError("年龄不能为负数")
if age > 150:
raise ValueError("年龄不能超过150岁")
return f"年龄设置为: {age}"
# 测试
try:
print(set_age(25)) # 正常
print(set_age(-5)) # ValueError
except (TypeError, ValueError) as e:
print(f"错误: {e}")
# 输出:
# 年龄设置为: 25
# 错误: 年龄不能为负数
异常链(Exception Chaining)
使用 raise ... from 可以创建异常链,保留原始异常信息。
def read_config(filename):
"""读取配置文件"""
try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError as e:
# 抛出新的异常,并保留原始异常
raise RuntimeError(f"无法加载配置文件: {filename}") from e
# 使用
try:
config = read_config("config.ini")
except RuntimeError as e:
print(f"错误: {e}")
print(f"原始异常: {e.__cause__}")
# 输出:
# 错误: 无法加载配置文件: config.ini
# 原始异常: [Errno 2] No such file or directory: 'config.ini'
不使用 from 的情况:
try:
risky_operation()
except ValueError as e:
raise RuntimeError("处理失败") # 不保留原始异常
主动抛出异常的场景
1. 参数验证:
def create_user(username, age, email):
"""创建用户,验证所有参数"""
if not username or len(username) < 3:
raise ValueError("用户名至少3个字符")
if not isinstance(age, int) or age < 18:
raise ValueError("年龄必须是18岁以上的整数")
if "@" not in email:
raise ValueError("邮箱格式不正确")
return {"username": username, "age": age, "email": email}
# 使用
try:
user = create_user("ab", 25, "test@example.com")
except ValueError as e:
print(f"创建用户失败: {e}")
# 输出: 创建用户失败: 用户名至少3个字符
2. 不支持的操作:
class ReadOnlyDict(dict):
"""只读字典"""
def __setitem__(self, key, value):
raise TypeError("不能修改只读字典")
def __delitem__(self, key):
raise TypeError("不能删除只读字典的元素")
# 使用
readonly_dict = ReadOnlyDict({"name": "张三"})
try:
readonly_dict["age"] = 25
except TypeError as e:
print(f"错误: {e}")
# 输出: 错误: 不能修改只读字典
3. 资源不可用:
class ConnectionPool:
"""连接池示例"""
def __init__(self, max_connections):
self.max_connections = max_connections
self.current_connections = 0
def get_connection(self):
if self.current_connections >= self.max_connections:
raise RuntimeError("连接池已满,无法获取新连接")
self.current_connections += 1
return "连接对象"
# 使用
pool = ConnectionPool(max_connections=2)
pool.get_connection()
pool.get_connection()
try:
pool.get_connection() # 第三个连接会失败
except RuntimeError as e:
print(f"错误: {e}")
# 输出: 错误: 连接池已满,无法获取新连接
自定义异常
创建自定义异常类
自定义异常类应该继承自 Exception 或其子类。
# 基本自定义异常
class InvalidPasswordError(Exception):
"""密码无效异常"""
pass
# 使用
def check_password(password):
if len(password) < 8:
raise InvalidPasswordError("密码长度至少8位")
return True
try:
check_password("abc")
except InvalidPasswordError as e:
print(f"密码错误: {e}")
# 输出: 密码错误: 密码长度至少8位
带参数的自定义异常:
class InvalidAgeError(Exception):
"""年龄无效异常"""
def __init__(self, age, message="年龄无效"):
self.age = age
self.message = message
super().__init__(self.message)
def __str__(self):
return f"{self.message} (提供的年龄: {self.age})"
def register_user(name, age):
if age < 18:
raise InvalidAgeError(age, "用户必须年满18岁")
if age > 120:
raise InvalidAgeError(age, "年龄超出合理范围")
return f"用户 {name} 注册成功"
# 使用
try:
register_user("张三", 15)
except InvalidAgeError as e:
print(f"注册失败: {e}")
print(f"提供的年龄: {e.age}")
# 输出:
# 注册失败: 用户必须年满18岁 (提供的年龄: 15)
# 提供的年龄: 15
异常类层次结构
可以创建异常层次结构,便于分类处理不同类型的异常。
# 基础异常类
class BankAccountError(Exception):
"""银行账户相关异常的基类"""
pass
# 具体异常类
class InsufficientFundsError(BankAccountError):
"""余额不足异常"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足: 当前余额 {balance}, 需要 {amount}")
class InvalidAmountError(BankAccountError):
"""金额无效异常"""
def __init__(self, amount):
self.amount = amount
super().__init__(f"金额无效: {amount}")
class AccountLockedError(BankAccountError):
"""账户被锁定异常"""
pass
# 银行账户类
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.is_locked = False
def withdraw(self, amount):
"""取款"""
if self.is_locked:
raise AccountLockedError("账户已被锁定,无法操作")
if amount <= 0:
raise InvalidAmountError(amount)
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance
def deposit(self, amount):
"""存款"""
if self.is_locked:
raise AccountLockedError("账户已被锁定,无法操作")
if amount <= 0:
raise InvalidAmountError(amount)
self.balance += amount
return self.balance
def lock(self):
"""锁定账户"""
self.is_locked = True
# 使用
account = BankAccount("张三", 1000)
def perform_operation(operation):
"""执行银行操作"""
try:
result = operation()
print(f"操作成功,当前余额: {result}")
except InsufficientFundsError as e:
print(f"取款失败: {e}")
except InvalidAmountError as e:
print(f"金额错误: {e}")
except AccountLockedError as e:
print(f"账户状态: {e}")
except BankAccountError as e:
print(f"银行操作错误: {e}")
# 测试各种场景
perform_operation(lambda: account.withdraw(500)) # 成功
perform_operation(lambda: account.withdraw(800)) # 余额不足
perform_operation(lambda: account.deposit(-100)) # 金额无效
account.lock()
perform_operation(lambda: account.withdraw(100)) # 账户锁定
# 输出:
# 操作成功,当前余额: 500
# 取款失败: 余额不足: 当前余额 500, 需要 800
# 金额错误: 金额无效: -100
# 账户状态: 账户已被锁定,无法操作
自定义异常的最佳实践
1. 提供清晰的错误信息:
class DataValidationError(Exception):
"""数据验证异常"""
def __init__(self, field, value, reason):
self.field = field
self.value = value
self.reason = reason
message = f"字段 '{field}' 验证失败: {reason} (当前值: {value})"
super().__init__(message)
# 使用
def validate_email(email):
if "@" not in email:
raise DataValidationError("email", email, "缺少 @ 符号")
if "." not in email.split("@")[1]:
raise DataValidationError("email", email, "域名部分缺少 . 符号")
return email
try:
validate_email("invalid-email")
except DataValidationError as e:
print(f"验证失败: {e}")
print(f"字段: {e.field}")
print(f"值: {e.value}")
print(f"原因: {e.reason}")
# 输出:
# 验证失败: 字段 'email' 验证失败: 缺少 @ 符号 (当前值: invalid-email)
# 字段: email
# 值: invalid-email
# 原因: 缺少 @ 符号
2. 添加额外信息和方法:
class APIError(Exception):
"""API 请求异常"""
def __init__(self, status_code, message, details=None):
self.status_code = status_code
self.message = message
self.details = details or {}
super().__init__(f"API 错误 {status_code}: {message}")
def is_client_error(self):
"""是否是客户端错误 (4xx)"""
return 400 <= self.status_code < 500
def is_server_error(self):
"""是否是服务器错误 (5xx)"""
return 500 <= self.status_code < 600
def __str__(self):
base_msg = super().__str__()
if self.details:
details_str = ", ".join(f"{k}={v}" for k, v in self.details.items())
return f"{base_msg} (详情: {details_str})"
return base_msg
# 使用
def api_request(endpoint):
"""模拟 API 请求"""
# 模拟不同的响应
if endpoint == "/user":
raise APIError(404, "用户不存在", {"endpoint": endpoint, "user_id": 123})
elif endpoint == "/auth":
raise APIError(401, "未授权访问")
elif endpoint == "/server":
raise APIError(500, "服务器内部错误")
return {"status": "success"}
# 测试
for endpoint in ["/user", "/auth", "/server"]:
try:
api_request(endpoint)
except APIError as e:
print(f"\n请求 {endpoint} 失败:")
print(f" 错误: {e}")
print(f" 客户端错误: {e.is_client_error()}")
print(f" 服务器错误: {e.is_server_error()}")
# 输出:
# 请求 /user 失败:
# 错误: API 错误 404: 用户不存在 (详情: endpoint=/user, user_id=123)
# 客户端错误: True
# 服务器错误: False
#
# 请求 /auth 失败:
# 错误: API 错误 401: 未授权访问
# 客户端错误: True
# 服务器错误: False
#
# 请求 /server 失败:
# 错误: API 错误 500: 服务器内部错误
# 客户端错误: False
# 服务器错误: True
3. 创建异常层次结构便于捕获:
# 基础异常
class PaymentError(Exception):
"""支付相关异常基类"""
pass
# 具体异常
class CreditCardError(PaymentError):
"""信用卡支付异常"""
pass
class PayPalError(PaymentError):
"""PayPal 支付异常"""
pass
class WeChatPayError(PaymentError):
"""微信支付异常"""
pass
def process_payment(method, amount):
"""处理支付"""
if method == "credit_card":
raise CreditCardError("信用卡已过期")
elif method == "paypal":
raise PayPalError("PayPal 账户余额不足")
elif method == "wechat":
raise WeChatPayError("微信支付签名错误")
raise PaymentError("不支持的支付方式")
# 使用
for method in ["credit_card", "paypal", "wechat", "alipay"]:
try:
process_payment(method, 100)
except CreditCardError as e:
print(f"{method}: 信用卡错误 - {e}")
except PayPalError as e:
print(f"{method}: PayPal 错误 - {e}")
except WeChatPayError as e:
print(f"{method}: 微信支付错误 - {e}")
except PaymentError as e:
print(f"{method}: 其他支付错误 - {e}")
# 输出:
# credit_card: 信用卡错误 - 信用卡已过期
# paypal: PayPal 错误 - PayPal 账户余额不足
# wechat: 微信支付错误 - 微信支付签名错误
# alipay: 其他支付错误 - 不支持的支付方式
异常处理最佳实践
1. 明确捕获具体异常
不好的做法:
try:
result = risky_operation()
except Exception: # 太宽泛
pass
好的做法:
try:
result = risky_operation()
except (ValueError, TypeError) as e: # 具体的异常类型
logger.error(f"操作失败: {e}")
2. 不要忽略异常
不好的做法:
try:
file = open("data.txt")
data = file.read()
except:
pass # 静默忽略所有异常
好的做法:
try:
file = open("data.txt")
data = file.read()
except FileNotFoundError as e:
logger.warning(f"文件不存在: {e}")
data = "" # 提供默认值
except IOError as e:
logger.error(f"读取文件失败: {e}")
raise # 重新抛出或处理
3. 使用 else 分离正常逻辑
不好的做法:
try:
file = open("data.txt")
data = file.read()
# 大量正常逻辑代码
processed_data = data.strip().upper()
result = analyze(processed_data)
except IOError as e:
logger.error(f"读取失败: {e}")
好的做法:
try:
file = open("data.txt")
data = file.read()
except IOError as e:
logger.error(f"读取失败: {e}")
data = None
else:
# 只在没有异常时执行
processed_data = data.strip().upper()
result = analyze(processed_data)
4. 适当使用 finally
资源清理:
connection = None
cursor = None
try:
connection = get_db_connection()
cursor = connection.cursor()
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
return results
except DatabaseError as e:
logger.error(f"数据库错误: {e}")
return []
finally:
# 确保资源总是被释放
if cursor:
cursor.close()
if connection:
connection.close()
5. 提供有用的错误信息
不好的做法:
try:
user = find_user(user_id)
except Exception:
raise ValueError("错误")
好的做法:
try:
user = find_user(user_id)
except DatabaseError as e:
raise ValueError(f"无法查找用户 ID {user_id}: {e}") from e
6. 记录异常信息
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def process_data(data):
try:
result = complex_operation(data)
return result
except ValueError as e:
logger.error(f"数据验证失败: {e}")
raise
except Exception as e:
logger.exception(f"未预期的错误: {e}") # 包含堆栈跟踪
raise
# 使用 exception() 记录完整堆栈
# 使用 error() 只记录错误信息
# 使用 critical() 记录严重错误
7. 早抛出,晚捕获
早抛出:
def divide(a, b):
"""在检测到问题立即抛出异常"""
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
晚捕获:
def main():
"""在最合适的地方捕获和处理异常"""
try:
# 多个可能抛出异常的操作
a = get_input()
b = get_input()
result = divide(a, b)
save_result(result)
except (ValueError, ZeroDivisionError) as e:
# 统一处理
show_error_message(f"计算失败: {e}")
logger.error(f"计算详情: a={a}, b={b}, error={e}")
8. 避免在 finally 中使用 return
不好的做法:
def get_data():
try:
return risky_operation()
except Exception:
return None
finally:
return "default" # 会覆盖前面的 return
好的做法:
def get_data():
try:
return risky_operation()
except Exception:
return None
finally:
cleanup_resources() # 只做清理,不返回
9. 使用上下文管理器(with 语句)
自动资源管理:
# 好的做法:自动关闭文件
def read_file(filename):
try:
with open(filename, 'r') as file:
return file.read()
except FileNotFoundError:
logger.error(f"文件不存在: {filename}")
return ""
# with 语句自动关闭文件,即使在读取时发生异常
# 自定义上下文管理器
from contextlib import contextmanager
@contextmanager
def db_transaction():
"""数据库事务上下文管理器"""
connection = get_connection()
try:
connection.begin_transaction()
yield connection
connection.commit()
except Exception as e:
connection.rollback()
logger.error(f"事务回滚: {e}")
raise
finally:
connection.close()
# 使用
with db_transaction() as conn:
conn.execute("INSERT INTO users VALUES (1, '张三')")
conn.execute("UPDATE accounts SET balance = 100")
# 如果发生异常,自动回滚
10. 创建有意义的自定义异常
# 业务相关的异常体系
class OrderError(Exception):
"""订单处理异常基类"""
pass
class OutOfStockError(OrderError):
"""库存不足异常"""
def __init__(self, product_id, requested, available):
self.product_id = product_id
self.requested = requested
self.available = available
super().__init__(
f"产品 {product_id} 库存不足: "
f"需要 {requested}, 可用 {available}"
)
class PaymentDeclinedError(OrderError):
"""支付被拒绝异常"""
pass
# 使用自定义异常提供清晰的业务错误信息
def place_order(product_id, quantity, payment_info):
try:
check_stock(product_id, quantity)
process_payment(payment_info)
confirm_order(product_id, quantity)
except OutOfStockError as e:
# 可以访问 e.product_id, e.requested, e.available
logger.error(f"订单失败: {e}")
suggest_alternative(product_id, e.available)
except PaymentDeclinedError as e:
logger.error(f"支付失败: {e}")
request_payment_method()
except OrderError as e:
logger.error(f"订单处理错误: {e}")
小结
本章节介绍了 Python 异常处理的完整知识体系:
核心概念:
- 异常是程序运行时的错误情况
- try-except-else-finally 语句提供完整的异常处理机制
- finally 块总是执行,适合资源清理
常见异常类型:
- 内置异常:ValueError, TypeError, IndexError, KeyError, FileNotFoundError 等
- 异常层次结构有助于分类处理
抛出异常:
- 使用 raise 语句抛出异常
- 可以创建异常链保留原始异常信息
- 参数验证和不支持操作时主动抛出异常
自定义异常:
- 继承 Exception 创建自定义异常
- 构建异常层次结构便于管理
- 添加额外信息和方法提高实用性
最佳实践:
- 明确捕获具体异常类型
- 不要忽略异常,提供适当处理
- 使用 else 分离正常逻辑
- 使用 finally 确保资源清理
- 提供有用的错误信息
- 记录异常信息便于调试
- 早抛出,晚捕获
- 避免在 finally 中使用 return
- 使用上下文管理器自动管理资源
- 创建有意义的自定义异常
下章预告: 下一章将介绍 Python 的模块与包,学习如何组织代码、创建可重用的模块,以及包的管理和发布。