在昨天的银行账户系统基础上,我们的程序功能虽然完整,但在遇到错误时只是简单地打印提示,这远远不够健壮。今天,我们将引入专业的异常处理机制与日志记录系统,让你的代码具备生产级的可靠性与可维护性。
🎯 今日目标
- 掌握
try-except-finally 结构的核心用法。
- 创建自定义异常类,使错误信息更具业务可读性。
- 熟练使用 Python 内置的
logging 模块记录不同级别的日志。
- 配置日志系统,实现同时输出到控制台和文件。
一、核心概念速览
| 概念 |
作用 |
关键语法/说明 |
| 异常捕获 |
优雅地处理程序运行时可能发生的错误。 |
try: ... except ValueError as e: ... |
| 自定义异常 |
定义与业务逻辑紧密相关的特定错误类型。 |
class InsufficientFundsError(Exception): ... |
| logging 模块 |
替代 print() 的专业日志工具,功能强大且可配置。 |
logging.info(), logging.error() |
| 日志级别 |
控制日志信息的详细程度,便于筛选和排查问题。 |
DEBUG < INFO < WARNING < ERROR < CRITICAL |
二、实战改造:银行账户系统
步骤 1:定义自定义异常类
首先,我们在 bank.py 中创建一套专属的异常类,这能极大地提升代码的可读性和错误处理的精准度。
# bank.py (新增)
class BankError(Exception):
"""银行系统基础异常"""
pass
class InsufficientFundsError(BankError):
"""余额不足异常"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足!当前余额: {balance:.2f},尝试取款: {amount:.2f}")
class InvalidAmountError(BankError):
"""无效金额异常"""
def __init__(self, amount, reason="金额必须为正数"):
self.amount = amount
super().__init__(f"无效金额 {amount}: {reason}")
优势:
- 结构化错误信息:异常对象携带了具体的
balance、amount 等上下文数据。
- 精准捕获:调用方可以使用
except InsufficientFundsError 来专门处理余额不足的情况,与其他错误区分开。
步骤 2:在业务逻辑中抛出异常
接下来,修改存款和取款方法,用抛出异常来代替简单的 return False 或 print。
# 修改 BankAccount.deposit()
def deposit(self, amount: float) -> bool:
if amount <= 0:
raise InvalidAmountError(amount) # 抛出异常,不再返回 False
self._balance += amount
return True # 成功
# 修改 SavingsAccount.withdraw()
def withdraw(self, amount: float) -> bool:
total = amount + self.WITHDRAW_FEE
if self._balance < total:
raise InsufficientFundsError(self._balance, total)
self._balance -= total
return True
关键转变:我们将错误处理的决策权交给了方法的调用者,这符合面向对象设计中“谁调用,谁负责”的清晰职责划分原则。
步骤 3:配置专业的日志系统
现在,引入 Python 强大的 logging 模块,它远比 print() 更适合生产环境。
# bank.py (顶部新增)
import logging
import os
# 创建 logs 目录
os.makedirs("logs", exist_ok=True)
# 配置日志格式
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 创建 logger
logger = logging.getLogger("BankSystem")
logger.setLevel(logging.DEBUG)
# 避免重复添加处理器(重要!)
if not logger.handlers:
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(LOG_FORMAT))
# 文件处理器
file_handler = logging.FileHandler("logs/bank.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(console_handler)
logger.addHandler(file_handler)
配置要点:
logger.handlers 检查:防止模块被多次导入时重复添加处理器,导致日志重复打印。
- 分级输出:控制台(
StreamHandler)只显示 INFO 级别及以上的日志,保持简洁;文件(FileHandler)则记录所有 DEBUG 级别及以上的详细信息,便于事后分析。
- 自动创建目录:使用
os.makedirs 确保日志文件目录存在。
步骤 4:在关键操作中记录与捕获日志
最后,我们在程序入口 (main.py) 中,使用 try-except 块安全地执行业务操作,并记录相应日志。
# 修改主程序入口(main.py)
from bank import SavingsAccount, CheckingAccount, BankError, InvalidAmountError, InsufficientFundsError
import logging
# 获取配置好的 logger
logger = logging.getLogger("BankSystem")
def safe_operation(account, operation, *args):
"""安全执行账户操作,统一处理异常和记录日志"""
try:
if operation == "deposit":
account.deposit(*args)
logger.info(f"✅ {account.owner} 存款 {args[0]:.2f} 元")
elif operation == "withdraw":
account.withdraw(*args)
logger.info(f"✅ {account.owner} 取款 {args[0]:.2f} 元")
return True
except InvalidAmountError as e:
logger.error(f"❌ 无效金额: {e}")
print(f"输入错误: {e}")
except InsufficientFundsError as e:
logger.warning(f"⚠️ 余额不足: {e}")
print(f"操作失败: {e}")
except Exception as e:
logger.critical(f"💥 未预期错误: {type(e).__name__}: {e}")
print("系统错误,请联系管理员")
return False
# 测试代码
if __name__ == "__main__":
savings = SavingsAccount("Alice")
# 正常操作
safe_operation(savings, "deposit", 1000)
safe_operation(savings, "withdraw", 100)
# 异常测试
safe_operation(savings, "deposit", -50) # 无效金额
safe_operation(savings, "withdraw", 2000) # 余额不足
运行后,你不仅会在控制台看到友好的提示,还会在 logs/bank.log 文件中找到所有操作的详细记录,这对于问题排查和系统审计至关重要。
三、运行结果与日志示例
执行上述程序后,生成的日志文件内容如下:

日志的价值:
- 运维人员:可以根据时间戳和错误级别快速定位问题发生的时间点。
- 审计人员:能够清晰地追踪每一笔资金的流入和流出。
- 开发人员:在调试阶段,可以通过查看
DEBUG 级别的日志了解程序的详细执行流程。
四、核心知识点与最佳实践总结
1. 异常处理的最佳实践
要精准捕获,而非笼统处理。
# ✅ 好做法:精准捕获特定业务异常
try:
account.withdraw(amount)
except InsufficientFundsError:
handle_low_balance() # 执行特定的处理逻辑
# ❌ 坏做法:捕获所有异常,可能掩盖真正的Bug
try:
account.withdraw(amount)
except Exception: # 这会让 KeyboardInterrupt(Ctrl+C)都无法终止程序!
pass
2. logging 与 print() 的对比
| 场景 |
logging |
print() |
| 生产环境 |
✅ 支持分级过滤、输出到文件、网络等 |
❌ 无法关闭,输出杂乱 |
| 调试 |
✅ 动态调整日志级别(如从 INFO 切到 DEBUG) |
❌ 需手动删除或注释掉打印语句 |
| 多模块应用 |
✅ 通过不同的 logger 名称进行隔离和管理 |
❌ 输出全部混在一起,难以区分来源 |
掌握 logging 模块是写出高质量 Python 代码的标志之一。
3. finally 子句的妙用
finally 块中的代码无论是否发生异常都会执行,常用于资源清理。
file = None
try:
file = open("data.txt")
process(file)
except IOError:
logger.error("文件读取失败")
finally:
if file:
file.close() # 确保文件句柄被释放,避免资源泄漏
在 Web 框架(如 Flask、Django)中,常用 finally 来确保数据库连接、文件锁等资源被正确释放。
五、巩固练习
-
添加新异常:实现一个 AccountNotFoundError,当尝试操作一个不存在的账户ID时抛出。
class AccountNotFoundError(BankError):
"""账户不存在异常"""
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"账户不存在: {account_id}")
-
增强日志记录:
- 在
BankAccount 类的 __init__ 方法中,记录新账户创建的日志(包含账户类型、ID、户主)。
- 在
SavingsAccount.withdraw 方法中,成功取款后记录包含手续费的明细日志。


-
日志轮转(进阶):防止日志文件无限增大,使用 RotatingFileHandler 实现日志轮转。
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler("logs/bank.log", maxBytes=1024*1024, backupCount=3)
# 当 bank.log 达到 1MB 时,会创建 bank.log.1, bank.log.2 等备份,最多保留3个备份文件。
总结:构建健壮系统的三层防御
一个可靠的系统通常遵循“预防 -> 处理 -> 记录”的三层策略:
- 预防:通过输入验证、类型检查等手段,尽可能在错误发生前拦截。
- 处理:对于无法预防的运行时错误,使用
try-except 进行优雅的异常捕获和恢复。
- 记录:通过详尽的日志系统,为问题排查、系统监控和审计追踪留下可靠依据。
恭喜你!通过引入异常处理和日志记录,你的银行系统已经脱胎换骨,具备了应对真实场景的健壮性。记住,可靠的代码不仅是能正确运行的代码,更是能在出错时清晰告知“哪里错了”和“为什么错”的代码。继续在云栈社区探索更多工程化实践,让你的开发技能更上一层楼。