找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2332

积分

0

好友

312

主题
发表于 半小时前 | 查看: 3| 回复: 0

在昨天的银行账户系统基础上,我们的程序功能虽然完整,但在遇到错误时只是简单地打印提示,这远远不够健壮。今天,我们将引入专业的异常处理机制日志记录系统,让你的代码具备生产级的可靠性与可维护性。

🎯 今日目标

  • 掌握 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}")

优势

  • 结构化错误信息:异常对象携带了具体的 balanceamount 等上下文数据。
  • 精准捕获:调用方可以使用 except InsufficientFundsError 来专门处理余额不足的情况,与其他错误区分开。

步骤 2:在业务逻辑中抛出异常

接下来,修改存款和取款方法,用抛出异常来代替简单的 return Falseprint

# 修改 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. loggingprint() 的对比

场景 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 来确保数据库连接、文件锁等资源被正确释放。

五、巩固练习

  1. 添加新异常:实现一个 AccountNotFoundError,当尝试操作一个不存在的账户ID时抛出。

    class AccountNotFoundError(BankError):
        """账户不存在异常"""
        def __init__(self, account_id):
            self.account_id = account_id
            super().__init__(f"账户不存在: {account_id}")
  2. 增强日志记录

    • BankAccount 类的 __init__ 方法中,记录新账户创建的日志(包含账户类型、ID、户主)。
    • SavingsAccount.withdraw 方法中,成功取款后记录包含手续费的明细日志。
      BankAccount类初始化日志记录代码
      SavingsAccount取款方法日志记录代码
  3. 日志轮转(进阶):防止日志文件无限增大,使用 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个备份文件。

总结:构建健壮系统的三层防御

一个可靠的系统通常遵循“预防 -> 处理 -> 记录”的三层策略:

  1. 预防:通过输入验证、类型检查等手段,尽可能在错误发生前拦截。
  2. 处理:对于无法预防的运行时错误,使用 try-except 进行优雅的异常捕获和恢复。
  3. 记录:通过详尽的日志系统,为问题排查、系统监控和审计追踪留下可靠依据。

恭喜你!通过引入异常处理和日志记录,你的银行系统已经脱胎换骨,具备了应对真实场景的健壮性。记住,可靠的代码不仅是能正确运行的代码,更是能在出错时清晰告知“哪里错了”和“为什么错”的代码。继续在云栈社区探索更多工程化实践,让你的开发技能更上一层楼。




上一篇:OpenClaw安装教程:通过AiPy Pro实现飞书/QQ远程控制
下一篇:腾讯云SkillHub上线引争议:镜像ClawHub数据致OpenClaw创始人服务器成本飙升
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-13 07:29 , Processed in 0.543690 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表