Python入门教程丨2.5 健壮性及异常处理,模拟ATM取款处理系统

5 阅读8分钟

1. 异常处理为什么重要?

鲁迅曾经说过:我想不写异常处理,或许是跟自己过不去罢!

相信大家肯定遇到这种情况:你写了一个程序,看起来很完美,能接受用户输入,还能做一些复杂计算。

你满怀期待地运行,突然——砰!崩溃了!

比如我想模拟一个ATM取款系统,代码如下:

def withdraw(balance, amount):
    return balance - amount

def main():
    balance = float(input("请输入账户余额:"))
    amount = float(input("请输入取款金额:"))
    new_balance = withdraw(balance, amount)
    print(f"取款成功!剩余余额:{new_balance}")

main()

看起来很简单吧?一个函数计算余额,一个输入输出逻辑,没毛病!那我们试试看运行。


**测试一:**如果输入 10050

取款成功!剩余余额:50.0

太棒了,功能完美!

测试二:如果手滑,输入了 qwe

ValueError: could not convert string to float

程序崩溃,结束了。

**测试三:**如果余额是 100,取款金额是 200

取款成功!剩余余额:-100.0

等等,这是什么鬼?负余额?ATM还倒贴钱?


问题来了:这样的代码能用吗?

  • 用户体验在哪里? 一输入错,没有任何提示,程序直接死掉了!
  • 程序逻辑正确吗? 明明没钱了,系统居然还能取款,难道不应该提醒用户余额不足吗?
  • 扩展性如何? 如果以后有更多功能,比如转账或支付,光靠这些代码,能扛住吗?

所以很多时候我们的代码看起来能正常运行,其实是建立在你了解代码的内部细节的基础上,真让用户去使用的话,天知道会出现什么问题!

因此,健壮性💪🏻是我们衡量程序好坏的一个重要指标。

👉🏻****健壮性指即使面对用户的错误操作或不可预知的异常情况,程序依然可以稳定运行,并提供清晰的反馈

一个健壮的程序需要:

  • 能处理各种异常情况,而不是直接退出。
  • 提供清晰的错误提示,帮助用户理解问题。
  • 确保系统逻辑完整性,避免出现负余额、超范围等异常状态。

2. 如何进行异常处理?

那么这个时候,我们就需要开始“叠甲”了,给代码套上面对各种问题的护甲。

所谓异常处理,就是一种程序设计中的重要机制,用于在程序执行过程中处理可能出现的异常情况或错误,如文件不存在、网络连接失败、用户输入不合法、除以零、内存不足等,让代码能够从容应对,甚至优雅地告诉用户“哪里出了问题”。

Python 提供了强大的异常处理工具,主要保留字如下:

  • try:尝试执行一段代码。
  • except:如果出错了,执行备用方案。
  • else:如果没有异常,执行这部分代码。
  • finally:无论有没有异常,最后总会执行。

2.1. tryexcept

基本的异常处理结构如下:

try:
# 尝试运行某段代码
except:
# 如果发生异常,执行这段代码

我们将其应用到刚刚的例子中,先解决一个最常见的问题——用户输入非法数据。

加入try-except改进后代码:

def withdraw(balance, amount):
    return balance - amount

def main():
    try:
        balance = float(input("请输入账户余额:"))
        amount = float(input("请输入取款金额:"))
        new_balance = withdraw(balance, amount)
        print(f"取款成功!剩余余额:{new_balance}")
    except ValueError:
        print("请输入有效的数字!")

main()

运行一下:输入 abc 时,程序不再崩溃,而是向用户输出

请输入有效的数字!

2.2. elsefinally

在异常处理结构中,elsefinally 提供了额外的控制:

  • **else**:当没有发生任何异常时执行。
  • **finally**:无论是否发生异常,总会执行的代码块(常用于资源释放)。

我们也来应用试试!

def main():
    try:
        balance = float(input("请输入账户余额:"))
        amount = float(input("请输入取款金额:"))
        new_balance = withdraw(balance, amount)
    except ValueError as e:
        print("请输入有效的数字!")
    else:
        print(f"取款成功!剩余余额:{new_balance}")
    finally:
        print("交易结束,谢谢使用!")

main()

在这段代码中,无论是否发生异常,都会输出"交易结束,谢谢使用!"

2.3. 捕获多种异常

在更复杂的场景中,程序可能遇到多种类型的错误。如果我们仅仅使用Exception则会捕获所有错误,这样太过宽泛,缺乏针对性

except Exception:
# 会把所有错误都捕获,包括无法预测的错误,可能掩盖真正的问题。

我们可以同时捕获不同类型的异常,提供针对性地处理。类似于if,提供出现某种错误情况后的特定处理方式。

我们给例子添加一个针对余额不足这种情况的处理:

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError("余额不足!")
    return balance - amount

def main():
    try:
        balance = float(input("请输入账户余额:"))
        amount = float(input("请输入取款金额:"))
        new_balance = withdraw(balance, amount)
        print(f"取款成功!剩余余额:{new_balance}")
    except ValueError as e:
        print("请输入有效的数字!")
    except Exception as e:
        print(f"未知错误:{e}")

main()
  • 如果余额不足,输出:错误:余额不足!
  • 如果输入非法数据,输出:"请输入有效的数字!"

2.4. 自定义异常类

直接使用内置异常类型(如 ValueError)虽然可以解决问题,但在复杂项目中,我们可能需要定义自己的异常,来提供更明确的错误分类。

比如在本例中,我们要区分“余额不足”和“账户冻结”等问题的话,自定义异常类就派上用场了!

增加自定义异常后的代码:

class InsufficientFundsError(Exception):
    """余额不足异常"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"余额不足!当前余额:{balance},取款金额:{amount}")

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount

def main():
    try:
        balance = float(input("请输入账户余额:"))
        amount = float(input("请输入取款金额:"))
        new_balance = withdraw(balance, amount)
        print(f"取款成功!剩余余额:{new_balance}")
    except InsufficientFundsError as e:
        print(e)
    except ValueError:
        print("错误:请输入有效的数字!")

main()

当余额不足时,代码输出: 余额不足!当前余额:100.0,取款金额:200.0

在本例中我们定义了一个InsufficientFundsError的类,来作为自定义异常类,关于类的具体知识我们将在第三章中学习。


3. 完善细节及注意事项

我们再加入一个循环来与用户交互,就得到了一个健壮、友好且扩展性强的 ATM 取款系统。最终代码如下:

class InsufficientFundsError(Exception):
    """余额不足异常"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"余额不足!当前余额:{balance:.2f},取款金额:{amount:.2f}")


def deposit(balance, amount):
    """存款功能"""
    if amount <= 0:
        raise ValueError("存款金额必须为正数!")
    return balance + amount


def withdraw(balance, amount):
    """处理取款操作"""
    if amount <= 0:
        raise ValueError("取款金额必须为正数!")
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount


def check_balance(balance):
    """查询余额"""
    print(f"当前账户余额为:{balance:.2f}")


def main():
    print("欢迎使用银行账户系统!")
    balance = 1000.0  # 初始账户余额

    while True:
        print("\n请选择操作:")
        print("1. 存款")
        print("2. 取款")
        print("3. 查询余额")
        print("4. 退出")

        try:
            choice = input("请输入操作编号(1-4):")
            if choice == "1":
                amount = float(input("请输入存款金额:"))
                balance = deposit(balance, amount)
                print(f"存款成功!当前余额:{balance:.2f}")
            elif choice == "2":
                amount = float(input("请输入取款金额:"))
                balance = withdraw(balance, amount)
                print(f"取款成功!当前余额:{balance:.2f}")
            elif choice == "3":
                check_balance(balance)
            elif choice == "4":
                print("感谢使用银行账户系统,再见!")
                break
            else:
                print("无效的操作编号,请重新输入!")
        except InsufficientFundsError as e:
            print(e)
        except ValueError as e:
            print(f"输入错误:{e}")
        except Exception as e:
            print(f"未知错误:{e}")
        finally:
            print("———操作完成———")


if __name__ == "__main__":
    main()

⚠️注意事项:

  1. 异常处理的范围不宜过大
def process_data(data):
    try:
        # 多段代码都可能抛出异常
        result = int(data[0])  # 可能抛出 IndexError 或 ValueError
        print(f"处理后的数据:{result * 2}")
        file = open("output.txt", "w")
        file.write(f"结果:{result}")
    except Exception as e:
        print(f"错误:{e}")  # 不清楚具体是哪一段代码抛出的异常

像这段代码中,IndexErrorValueError 和文件操作错误都被混杂在一个 except 中,可读性低。

  1. 不能在 finally 中修改返回值
def calculate():
    try:
        return 10 / 2  # 正常返回值是 5.0
    finally:
        return "执行完成!"  # 覆盖了原始返回值

result = calculate()
print(result)  # 输出:"执行完成!"

在这个例子中,finally 中的 return 覆盖了 try 中的返回值。即使 try 中的计算逻辑正确,也无法返回正确的结果。


妥善使用异常处理,你就能:

增强健壮性:让程序即使面对错误也能运行,而不是崩溃。

提高可维护性:通过自定义异常和清晰的错误处理逻辑,让代码更易扩展。

提升用户体验:用户遇到错误时能够获得有用的信息,而不是一脸懵逼地看着崩溃提示。

🛠️ 记住:代码不仅要能跑,还要跑得“稳”!