Day 17:【99天精通Python】异常处理 - 让程序稳如泰山

前言

欢迎来到第17天!

在编程的世界里,不出 bug 是不可能的。即便是最顶尖的程序员,也无法保证代码永远不出错。用户可能会输入非法数据,文件可能突然被删除,网络可能会断开。

如果程序一遇到错误就直接崩溃(闪退),用户体验会非常差。异常处理 (Exception Handling) 就是给程序穿上一层"防弹衣",当意外发生时,我们能优雅地捕获错误,并给出友好的提示,而不是让程序直接挂掉。

本节内容:

  • 什么是异常?
  • try...except 基本结构
  • 捕获多种异常
  • elsefinally 的作用
  • 主动抛出异常 raise
  • 自定义异常类
  • 实战练习

一、什么是异常 (Exception)?

异常是指程序在运行过程中发生的错误,它会打断指令的正常执行。

常见的异常类型

  • SyntaxError: 语法错误(通常在运行前就会报错)。
  • NameError: 尝试访问一个未定义的变量。
  • TypeError: 类型不匹配(例如数字 + 字符串)。
  • ZeroDivisionError: 除数为 0。
  • IndexError: 列表索引越界。
  • KeyError: 字典中不存在该键。
  • FileNotFoundError: 打开不存在的文件。

二、基本语法:try…except

我们使用 try 块包含可能出错的代码,用 except 块来处理错误。

2.1 基本结构

try:
    # 可能会报错的代码
    num = int(input("请输入一个数字: "))
    result = 10 / num
    print(f"计算结果: {result}")
except ZeroDivisionError:
    # 当发生除以0错误时执行
    print("错误:除数不能为0!")
except ValueError:
    # 当输入的不是数字时执行
    print("错误:请输入有效的整数!")

2.2 获取异常信息

有时我们需要知道具体的报错信息,可以使用 as 关键字。

try:
    print(1 / 0)
except ZeroDivisionError as e:
    print(f"捕获到错误: {e}")  # 捕获到错误: division by zero

2.3 捕获所有异常 (慎用)

可以使用 Exception 捕获所有类型的运行时错误。但通常不推荐这样做,因为这会掩盖一些意想不到的 bug(比如变量名写错)。

try:
    # 复杂逻辑
    pass
except Exception as e:
    print(f"发生未知错误: {e}")

三、完善结构:else 与 finally

异常处理还有两个可选的子句:elsefinally

  • else: 当 try没有发生异常时执行。
  • finally: 无论是否发生异常,最终都会执行(常用于清理资源,如关闭文件、断开数据库)。
def division_test(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("Error: 除数不能为0")
    else:
        print(f"Success: 结果是 {result}")
    finally:
        print("--- 执行完毕 ---")

print("第一次测试:")
division_test(10, 2)

print("\n第二次测试:")
division_test(10, 0)

运行结果

第一次测试:
Success: 结果是 5.0
--- 执行完毕 ---

第二次测试:
Error: 除数不能为0
--- 执行完毕 ---

四、主动抛出异常:raise

有时候,并不是代码出错了,而是业务逻辑不满足,我们需要主动报错,通知调用者。这时使用 raise 关键字。

def set_age(age):
    if age < 0 or age > 150:
        # 主动抛出一个值错误
        raise ValueError("年龄必须在 0 到 150 之间")
    print(f"年龄设置为: {age}")

try:
    set_age(200)
except ValueError as e:
    print(f"设置失败: {e}")

五、自定义异常

虽然 Python 内置了很多异常,但在大型项目中,为了更清晰地表达业务错误,我们通常会自定义异常类。只需要继承 Exception 类即可。

# 定义一个"余额不足"异常
class InsufficientFundsError(Exception):
    pass

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            # 抛出自定义异常
            raise InsufficientFundsError(f"余额不足!当前: {self.balance}, 需要: {amount}")
        self.balance -= amount
        print(f"取款成功,剩余: {self.balance}")

account = BankAccount(100)

try:
    account.withdraw(200)
except InsufficientFundsError as e:
    print(f"交易拒绝: {e}")

六、实战练习

练习1:健壮的整数输入器

编写一个函数 get_integer(prompt),循环提示用户输入内容,直到用户输入一个合法的整数为止。

def get_integer(prompt):
    while True:
        user_input = input(prompt)
        try:
            value = int(user_input)
            return value  # 成功转换,返回结果并退出循环
        except ValueError:
            print(f"输入无效:'{user_input}' 不是一个整数,请重试。")

# 测试
age = get_integer("请输入你的年龄: ")
print(f"你的年龄是: {age}")

练习2:安全的文件读取

编写一个程序读取文件,如果文件不存在,提示用户;如果文件编码错误,也提示用户;无论如何最后都要打印"操作结束"。

def safe_read(filename):
    try:
        with open(filename, "r", encoding="utf-8") as f:
            print(f.read())
    except FileNotFoundError:
        print(f"错误: 文件 '{filename}' 未找到。")
    except UnicodeDecodeError:
        print(f"错误: 文件 '{filename}' 编码格式不对,无法读取。")
    except Exception as e:
        print(f"发生未知错误: {e}")
    finally:
        print("--- 读取操作结束 ---")

# 测试
safe_read("not_exist.txt")

七、常见问题

Q1:try 块里应该放多少代码?

越少越好。只把可能报错的那几行关键代码放进去。如果在 try 里放了几百行代码,一旦报错,你很难定位到底是哪一行出了问题。

Q2:except 可以不写类型吗?

可以写 except:,但这等同于捕获所有异常(包括 Ctrl+C 中断)。这是一种极坏的编程习惯,强烈建议至少写 except Exception:

Q3:returnfinally 谁先执行?

如果 try 块中有 returnfinally 会在 return 之前执行。千万不要在 finally 里面写 return,否则会覆盖掉原本的返回值。


八、小结

无错误

发生异常

异常处理流程

try: 执行代码

执行 else (可选)

匹配 except 类型?

执行 except 块 (处理错误)

向上抛出异常 (程序崩溃)

执行 finally (无论如何都执行)

其他核心

raise: 主动报错

自定义异常: 继承 Exception

关键要点

  1. 预判错误:用 try...except 包裹可能出错的代码。
  2. 精准捕获:尽量指定具体的异常类型(如 ValueError),避免使用笼统的 Exception
  3. 善后处理:资源释放(如关闭文件)一定要放在 finally 中。
  4. 业务逻辑:不满足条件时,可以用 raise 主动中断程序。

九、课后作业

  1. 简易除法器:编写程序接收两个用户输入的数字进行除法运算。需要处理:输入非数字、除数为0、以及未知错误。
  2. 列表越界保护:编写一个函数 safe_get(lst, index),接收列表和索引。如果索引有效,返回对应的元素;如果索引越界,捕获 IndexError 并返回 None(而不是报错)。
  3. 用户注册系统:定义一个 register(username, password) 函数。
    • 如果用户名长度小于3,抛出自定义异常 UsernameTooShortError
    • 如果密码长度小于6,抛出自定义异常 PasswordTooShortError
    • 编写调用代码,捕获并打印这些错误。

下节预告

Day 18:常用内置模块 (JSON, Datetime, Random) - 之前简单提过标准库,明天我们将深入讲解开发中最离不开的几个模块,学会处理时间、数据交换和随机事件。


系列导航

  • 上一篇:Day 16 - 面向对象编程(下)
  • 下一篇:Day 18 - 常用内置模块(待更新)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐