2.8 Python 中的异常处理与自定义异常
Python异常处理通过try、except、else、finally等关键字捕获和处理程序运行时的错误。内置异常如ValueError、FileNotFoundError等覆盖常见错误场景,开发者也可通过继承Exception类创建自定义异常(如银行账户中的InsufficientFundsError)。异常处理最佳实践包括精准捕获异常类型、使用logging记录错误、合理利用上下文管理器(wi
引言:构建健壮的程序
在软件开发中,错误和异常是不可避免的。无论是用户输入错误、文件不存在、网络连接中断还是其他意外情况,程序都需要能够优雅地处理这些问题。Python提供了强大的异常处理机制,允许开发者捕获、处理和抛出异常,从而编写出更加健壮和可靠的程序。
在本节中,我们将深入探讨Python的异常处理机制,学习如何使用内置异常,创建自定义异常,以及如何在银行账户管理系统中应用异常处理来构建更加可靠的应用程序。
第一部分:Python异常处理基础
1.1 什么是异常?
异常是程序在执行过程中发生的错误或意外情况的信号。当Python解释器遇到无法正常处理的情况时,它会创建一个异常对象并"抛出"(raise)它。如果异常没有被捕获和处理,程序将终止并显示错误信息。
1.2 异常处理语法
Python使用try、except、else和finally关键字来处理异常。
# 基本异常处理结构
try:
# 可能引发异常的代码
result = 10 / 0
except ZeroDivisionError:
# 处理特定异常
print("不能除以零!")
except Exception as e:
# 处理其他异常
print(f"发生未知错误: {e}")
else:
# 如果没有异常发生,执行这里的代码
print("计算成功!")
finally:
# 无论是否发生异常,都会执行的代码
print("执行清理操作")
1.3 常见的异常类型
Python提供了丰富的内置异常类型,以下是一些常见的异常:
ZeroDivisionError:除零错误ValueError:值错误,如将字符串转换为整数时失败TypeError:类型错误,如对整数和字符串进行加法操作FileNotFoundError:文件不存在错误IndexError:索引错误,如访问列表不存在的索引KeyError:键错误,如访问字典不存在的键AttributeError:属性错误,如访问不存在的属性ImportError:导入错误,如导入不存在的模块
第二部分:抛出异常
除了处理异常,有时我们还需要主动抛出异常。这可以通过raise语句实现。
2.1 抛出内置异常
def validate_age(age):
if age < 0:
raise ValueError("年龄不能为负数")
if age < 18:
raise ValueError("年龄未满18岁")
return True
try:
validate_age(-5)
except ValueError as e:
print(e) # 输出: 年龄不能为负数
2.2 重新抛出异常
有时我们希望在处理异常后再次抛出相同的异常,或者抛出另一个异常。
def process_file(filename):
try:
with open(filename, 'r') as f:
content = f.read()
return content
except FileNotFoundError:
print("文件不存在,使用默认内容")
# 重新抛出异常
raise
except IOError as e:
print(f"文件读取错误: {e}")
# 抛出新的异常
raise RuntimeError("无法处理文件") from e
2.3 异常链
在Python 3中,可以使用raise from语法来保留原始异常信息,形成异常链。
try:
# 尝试操作
config = load_config()
except FileNotFoundError as e:
raise RuntimeError("配置加载失败") from e
第三部分:自定义异常
虽然Python提供了丰富的内置异常,但有时我们需要创建自定义异常来表示特定的错误情况。
3.1 创建自定义异常
自定义异常通常继承自Exception类或其子类。
class BankAccountError(Exception):
"""银行账户异常基类"""
pass
class InsufficientFundsError(BankAccountError):
"""余额不足异常"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足。当前余额: ${balance:.2f}, 尝试取款: ${amount:.2f}")
class InvalidAmountError(BankAccountError):
"""无效金额异常"""
def __init__(self, amount):
self.amount = amount
super().__init__(f"无效金额: ${amount:.2f}。金额必须为正数")
class AccountNotFoundError(BankAccountError):
"""账户未找到异常"""
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"账户未找到: {account_id}")
class TransactionLimitExceededError(BankAccountError):
"""交易限额超出异常"""
def __init__(self, limit, attempted_amount):
self.limit = limit
self.attempted_amount = attempted_amount
super().__init__(f"交易限额超出。限额: ${limit:.2f}, 尝试交易: ${attempted_amount:.2f}")
3.2 使用自定义异常
class BankAccount:
def __init__(self, account_holder, balance=0, daily_limit=1000):
self.account_holder = account_holder
self.balance = balance
self.daily_limit = daily_limit
self.daily_withdrawals = 0
def withdraw(self, amount):
if amount <= 0:
raise InvalidAmountError(amount)
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
if self.daily_withdrawals + amount > self.daily_limit:
raise TransactionLimitExceededError(self.daily_limit, self.daily_withdrawals + amount)
self.balance -= amount
self.daily_withdrawals += amount
return True
# 使用示例
account = BankAccount("Alice", 1000, 500)
try:
account.withdraw(600) # 这会抛出TransactionLimitExceededError
except BankAccountError as e:
print(f"取款失败: {e}")
第四部分:异常处理的最佳实践
4.1 具体异常优于泛化异常
尽量避免捕获所有异常,而应该捕获具体的异常类型。
# 不推荐
try:
# 一些操作
except Exception as e:
print(e)
# 推荐
try:
# 一些操作
except (ValueError, TypeError) as e:
print(e)
4.2 使用异常链
在Python 3中,可以使用raise from来保留原始异常信息。
try:
# 尝试操作
except FileNotFoundError as e:
raise RuntimeError("无法处理文件") from e
4.3 日志记录异常
使用logging模块记录异常信息,而不是仅仅打印到控制台。
import logging
logging.basicConfig(level=logging.ERROR)
try:
# 一些操作
except Exception as e:
logging.exception("发生异常")
4.4 使用else和finally
合理使用else和finally子句可以使代码更加清晰。
try:
file = open('data.txt', 'r')
except IOError as e:
print(f"文件打开失败: {e}")
else:
try:
data = file.read()
# 处理数据
finally:
file.close() # 确保文件总是被关闭
第五部分:上下文管理器与异常处理
Python的上下文管理器(with语句)可以与异常处理结合使用,确保资源被正确释放。
5.1 使用with语句管理资源
# 使用with语句自动管理文件资源
try:
with open('data.txt', 'r') as file:
data = file.read()
# 处理数据
except FileNotFoundError:
print("文件不存在")
except IOError as e:
print(f"文件读取错误: {e}")
5.2 实现自定义上下文管理器
我们可以实现自己的上下文管理器来处理特定资源的清理工作。
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None
def __enter__(self):
self.connection = connect_to_database(self.connection_string)
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
self.connection.close()
if exc_type is not None:
print(f"数据库操作发生错误: {exc_val}")
return False # 不抑制异常
# 使用示例
try:
with DatabaseConnection("mysql://user:pass@localhost/db") as conn:
# 执行数据库操作
result = conn.execute_query("SELECT * FROM accounts")
except DatabaseError as e:
print(f"数据库错误: {e}")
第六部分:综合实战 —— 银行账户系统的异常处理
现在,让我们将异常处理应用到银行账户管理系统中。
import logging
from abc import ABC, abstractmethod
from datetime import datetime
import random
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('bank_system.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger('BankSystem')
# 自定义异常
class BankSystemError(Exception):
"""银行系统异常基类"""
pass
class AccountError(BankSystemError):
"""账户相关异常"""
pass
class InsufficientFundsError(AccountError):
"""余额不足异常"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足。当前余额: ${balance:.2f}, 尝试取款: ${amount:.2f}")
class InvalidAmountError(AccountError):
"""无效金额异常"""
def __init__(self, amount):
self.amount = amount
super().__init__(f"无效金额: ${amount:.2f}。金额必须为正数")
class AccountNotFoundError(AccountError):
"""账户未找到异常"""
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"账户未找到: {account_id}")
class TransactionError(BankSystemError):
"""交易相关异常"""
pass
class DailyLimitExceededError(TransactionError):
"""每日限额超出异常"""
def __init__(self, limit, attempted_amount):
self.limit = limit
self.attempted_amount = attempted_amount
super().__init__(f"每日限额超出。限额: ${limit:.2f}, 尝试交易: ${attempted_amount:.2f}")
# 银行账户基类
class BankAccount(ABC):
def __init__(self, account_holder, initial_balance=0, daily_limit=1000):
if not isinstance(account_holder, str) or not account_holder.strip():
raise ValueError("账户持有人必须是非空字符串")
if initial_balance < 0:
raise InvalidAmountError(initial_balance)
self._account_holder = account_holder
self._balance = initial_balance
self._account_number = self._generate_account_number()
self._created_date = datetime.now()
self._daily_limit = daily_limit
self._daily_withdrawals = 0
self._last_reset_date = datetime.now().date()
logger.info(f"创建账户: {self._account_number}, 持有人: {self._account_holder}")
def _generate_account_number(self):
return f"ACC{random.randint(100000, 999999)}"
def _reset_daily_counter(self):
"""重置每日计数器"""
today = datetime.now().date()
if today != self._last_reset_date:
self._daily_withdrawals = 0
self._last_reset_date = today
@property
def account_number(self):
return self._account_number
@property
def account_holder(self):
return self._account_holder
@property
def balance(self):
return self._balance
def deposit(self, amount):
"""存款方法"""
try:
if amount <= 0:
raise InvalidAmountError(amount)
self._balance += amount
logger.info(f"账户 {self._account_number} 存款 ${amount:.2f} 成功")
return True
except InvalidAmountError as e:
logger.error(f"存款失败: {e}")
raise
except Exception as e:
logger.exception(f"存款过程中发生未知错误: {e}")
raise BankSystemError("存款操作失败") from e
@abstractmethod
def withdraw(self, amount):
"""取款抽象方法,子类必须实现"""
pass
def get_account_info(self):
return f"{self._account_number} - {self._account_holder}: ${self._balance:.2f}"
# 具体账户实现
class SavingsAccount(BankAccount):
def __init__(self, account_holder, initial_balance=0, daily_limit=1000, min_balance=50):
super().__init__(account_holder, initial_balance, daily_limit)
self._min_balance = min_balance
def withdraw(self, amount):
"""取款实现"""
try:
self._reset_daily_counter()
if amount <= 0:
raise InvalidAmountError(amount)
if self._balance - amount < self._min_balance:
raise InsufficientFundsError(self._balance, amount)
if self._daily_withdrawals + amount > self._daily_limit:
raise DailyLimitExceededError(self._daily_limit, self._daily_withdrawals + amount)
self._balance -= amount
self._daily_withdrawals += amount
logger.info(f"账户 {self._account_number} 取款 ${amount:.2f} 成功")
return True
except (InvalidAmountError, InsufficientFundsError, DailyLimitExceededError) as e:
logger.error(f"取款失败: {e}")
raise
except Exception as e:
logger.exception(f"取款过程中发生未知错误: {e}")
raise BankSystemError("取款操作失败") from e
# 银行类
class Bank:
def __init__(self, name):
self.name = name
self._accounts = {}
logger.info(f"创建银行: {name}")
def add_account(self, account):
"""添加账户"""
try:
if not isinstance(account, BankAccount):
raise TypeError("只能添加BankAccount对象")
if account.account_number in self._accounts:
raise ValueError(f"账户已存在: {account.account_number}")
self._accounts[account.account_number] = account
logger.info(f"添加账户到银行: {account.account_number}")
return True
except (TypeError, ValueError) as e:
logger.error(f"添加账户失败: {e}")
raise
except Exception as e:
logger.exception(f"添加账户过程中发生未知错误: {e}")
raise BankSystemError("添加账户失败") from e
def get_account(self, account_number):
"""获取账户"""
try:
account = self._accounts.get(account_number)
if account is None:
raise AccountNotFoundError(account_number)
return account
except AccountNotFoundError as e:
logger.error(f"获取账户失败: {e}")
raise
except Exception as e:
logger.exception(f"获取账户过程中发生未知错误: {e}")
raise BankSystemError("获取账户失败") from e
def transfer(self, from_account_number, to_account_number, amount):
"""转账"""
try:
from_account = self.get_account(from_account_number)
to_account = self.get_account(to_account_number)
# 尝试从源账户取款
if from_account.withdraw(amount):
# 如果取款成功,向目标账户存款
to_account.deposit(amount)
logger.info(f"转账成功: 从 {from_account_number} 到 {to_account_number} 金额 ${amount:.2f}")
return True
return False
except AccountError as e:
logger.error(f"转账失败: {e}")
raise
except Exception as e:
logger.exception(f"转账过程中发生未知错误: {e}")
# 尝试回滚操作(简化实现)
try:
# 这里应该有更完整的回滚机制
logger.warning("尝试回滚转账操作")
except Exception as rollback_error:
logger.error(f"回滚操作失败: {rollback_error}")
raise BankSystemError("转账操作失败") from e
# 演示使用
if __name__ == "__main__":
try:
# 创建银行
bank = Bank("Python银行")
# 创建账户
account1 = SavingsAccount("Alice", 1000, daily_limit=500, min_balance=100)
account2 = SavingsAccount("Bob", 500, daily_limit=300, min_balance=50)
# 添加账户到银行
bank.add_account(account1)
bank.add_account(account2)
print("初始状态:")
print(f"账户1: {account1.get_account_info()}")
print(f"账户2: {account2.get_account_info()}")
# 进行一些操作
print("\n1. 正常存款:")
account1.deposit(200)
print(f"账户1余额: ${account1.balance:.2f}")
print("\n2. 正常取款:")
account1.withdraw(100)
print(f"账户1余额: ${account1.balance:.2f}")
print("\n3. 尝试取款过多:")
try:
account1.withdraw(2000) # 这会抛出InsufficientFundsError
except InsufficientFundsError as e:
print(f"取款失败: {e}")
print("\n4. 尝试无效金额:")
try:
account1.deposit(-100) # 这会抛出InvalidAmountError
except InvalidAmountError as e:
print(f"存款失败: {e}")
print("\n5. 转账操作:")
bank.transfer(account1.account_number, account2.account_number, 300)
print(f"转账后账户1余额: ${account1.balance:.2f}")
print(f"转账后账户2余额: ${account2.balance:.2f}")
print("\n6. 尝试向不存在的账户转账:")
try:
bank.transfer(account1.account_number, "INVALID_ACC", 100)
except AccountNotFoundError as e:
print(f"转账失败: {e}")
except BankSystemError as e:
print(f"银行系统错误: {e}")
except Exception as e:
print(f"发生未知错误: {e}")
finally:
print("\n程序执行完成")
第七部分:测试与异常处理
良好的异常处理应该与测试相结合,确保异常情况被正确处理。
7.1 使用unittest测试异常
import unittest
class TestBankAccount(unittest.TestCase):
def setUp(self):
self.account = SavingsAccount("Test User", 1000)
def test_withdraw_insufficient_funds(self):
"""测试余额不足异常"""
with self.assertRaises(InsufficientFundsError):
self.account.withdraw(2000)
def test_deposit_negative_amount(self):
"""测试存款金额为负异常"""
with self.assertRaises(InvalidAmountError):
self.account.deposit(-100)
def test_transfer_to_nonexistent_account(self):
"""测试向不存在账户转账异常"""
bank = Bank("Test Bank")
bank.add_account(self.account)
with self.assertRaises(AccountNotFoundError):
bank.transfer(self.account.account_number, "NONEXISTENT", 100)
if __name__ == '__main__':
unittest.main()
7.2 使用pytest测试异常
import pytest
def test_withdraw_insufficient_funds():
"""使用pytest测试余额不足异常"""
account = SavingsAccount("Test User", 1000)
with pytest.raises(InsufficientFundsError) as excinfo:
account.withdraw(2000)
assert "余额不足" in str(excinfo.value)
第八部分:常见面试题深度解析
8.1 Q:except和finally的执行顺序是怎样的?
A:当try块中发生异常时,首先执行匹配的except块,然后执行finally块。如果没有发生异常,执行else块,然后执行finally块。
8.2 Q:如何创建自定义异常?为什么要创建自定义异常?
A:自定义异常通过继承Exception类创建。创建自定义异常可以:
- 提供更具体的错误信息
- 更好地组织异常层次结构
- 让调用者能够捕获特定的异常
- 提供更多的上下文信息
8.3 Q:raise语句的作用是什么?
A:raise语句用于主动抛出异常。它可以抛出当前上下文中捕获的异常,或者一个新的异常。
8.4 Q:什么是异常链?如何使用?
A:异常链是指在处理一个异常时抛出另一个异常,同时保留原始异常的信息。使用raise NewException from OriginalException语法。
8.5 Q:什么时候应该使用异常处理?
A:异常处理应该用于处理预期可能发生的错误情况,而不是用于控制程序流程。常见的用例包括:
- 用户输入验证
- 文件I/O操作
- 网络请求
- 数据库操作
- 资源管理
结语与思考
异常处理是编写健壮程序的关键部分。通过合理的异常处理,我们可以使程序在遇到错误时 gracefully 降级,而不是突然崩溃。自定义异常可以帮助我们更好地表达错误信息,提高代码的可读性和可维护性。
在我们的银行账户系统实战中,我们看到了如何通过异常处理来管理账户操作中的各种错误情况,确保系统的稳定性和数据的一致性。我们还学习了如何结合日志记录、测试和上下文管理器来构建更加可靠的应用程序。
思考题:
- 如果银行系统需要支持多语言错误消息,应该如何设计异常类?
- 如何编写一个全局异常处理器来捕获所有未处理的异常?
- 在分布式系统中,异常处理有哪些特殊的考虑因素?
- 如何测试异常处理代码?
- 如何处理异步代码中的异常?
更多推荐

所有评论(0)