第6章 6.1 try/except/finally 异常处理

🎯 开场:程序也会"生病"

上一章我们学会了用 JSON/CSV/YAML 这些格式存数据、取数据,手里终于有了"数据瑞士军刀"。但你有没有遇到过这种情况——

# 你的代码
data = open("不存在的文件.txt", "r", encoding="utf-8")
content = data.read()
print(content)

运行结果:

FileNotFoundError: [Errno 2] No such file or directory: '不存在的文件.txt'

程序直接崩溃,红字一片。这感觉就像你信心满满去超市买菜,结果到了发现超市倒闭了——白跑一趟。

痛点来了:
1. 用户输入了一个不存在的文件路径,程序直接炸了
2. 网络请求超时,程序直接挂掉
3. 读取的数据格式不对,整段代码都废掉

这章学完,你能:给程序穿上"防护服",让它在出错时不是崩溃而是优雅地处理。就像超市倒闭了你还能去隔壁菜市场,而不是站在原地干瞪眼。


🧱 基础:异常处理的三个金刚

异常处理是什么?

类比时间: 就像开车系安全带。正常情况下你不需要它,但万一出事,它能保命。异常处理就是程序的"安全带"。

1. try...except:捕获异常

try:
# 尝试执行这段代码
result = 10 / 0  # 这行会出错
except ZeroDivisionError:
# 如果出错了,执行这段
print("糟糕,除数不能是0!")

输出:

糟糕,除数不能是0!

说白了: try 就是"试试这段代码能不能跑",except 就是"跑砸了怎么办"。

2. except 多种异常:分别处理

try:
# 尝试执行
num = int(input("输入一个数字:"))  # 用户可能输入"abc"
result = 100 / num
except ValueError:
print("喂,我说的是数字!")
except ZeroDivisionError:
print("除数不能是0,知道吗?")

用户输入 abc

输入一个数字:abc
喂,我说的是数字!

用户输入 0

输入一个数字:0
除数不能是0,知道吗?

注意! 异常类型是有层级的,比如 FileNotFoundErrorOSError 的子类。如果你写成:

except OSError:
print("文件操作出错")
except FileNotFoundError:
print("文件找不到")  # 这行永远执行不到!

第二个 except 永远不会被触发,因为 FileNotFoundError 已经被前面的 OSError 捕获了。

3. except 带变量:拿到错误信息

try:
with open("data.json", "r", encoding="utf-8") as f:
    data = json.load(f)
except FileNotFoundError as e:
print(f"文件不存在:{e}")
except json.JSONDecodeError as e:
print(f"JSON格式不对:{e}")

as e 的意思是"把错误对象取个名字叫 e",这样你就能看到具体是什么错。

4. else:不出错才执行

try:
result = 10 / 2
except ZeroDivisionError:
print("出错了")
else:
print(f"计算成功,结果是 {result}")

输出:

计算成功,结果是 5.0

什么时候用? 当你想区分"成功执行"和"异常处理"两种情况时。

配图1 - 配图1

5. finally:无论如何都执行

try:
file = open("test.txt", "r", encoding="utf-8")
content = file.read()
except FileNotFoundError:
print("文件不存在")
finally:
# 不管出没出错,这段都会执行
print("无论怎样,程序跑完了")

重点! finally 最常用的场景是关闭文件、释放连接等"清理工作"。

# 推荐写法:with 自动关闭文件,但 finally 适合更通用的场景
try:
result = 10 / 0
except ZeroDivisionError:
print("出错了")
finally:
print("清理资源...")

6. raise:主动抛出异常

有时候不是你踩坑了,而是你主动告诉别人"这里有问题"

def divide(a, b):
if b == 0:
    raise ValueError("除数不能是0!")
return a / b

try:
result = divide(10, 0)
except ValueError as e:
print(f"捕获到错误:{e}")

输出:

捕获到错误:除数不能是0!

类比: raise 就像是机场安检,发现可疑物品直接拦下来,而不是让飞机起飞后才发现问题。


🔥 实战:3 个小项目

项目 1:计算器(5 分钟)

def safe_divide(a, b):
"""安全的除法运算"""
try:
    result = a / b
except ZeroDivisionError:
    return "错误:除数不能是0"
except TypeError:
    return "错误:请输入数字"
else:
    return f"结果:{result}"

# 测试
print(safe_divide(10, 2))
print(safe_divide(10, 0))
print(safe_divide(10, "2"))

输出:

结果:5.0
错误:除数不能是0
错误:请输入数字

一句话解释:try/except 把除法包起来,出了问题返回错误提示而不是崩溃。


项目 2:读取 CSV 文件并计算(15 分钟)

从 CSV 文件读取销售数据,计算总额并处理各种错误。

import csv

def calculate_sales(file_path):
"""读取CSV文件并计算销售总额"""
total = 0
errors = []

try:
    with open(file_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row_num, row in enumerate(reader, 1):
            try:
                # 尝试把数量和单价转成数字
                quantity = float(row["数量"])
                price = float(row["单价"])
                total += quantity * price
            except KeyError as e:
                errors.append(f"第{row_num}行:缺少字段 {e}")
            except ValueError as e:
                errors.append(f"第{row_num}行:数据格式错误 {e}")
except FileNotFoundError:
    return f"错误:文件 '{file_path}' 不存在"
except PermissionError:
    return f"错误:没有权限读取 '{file_path}'"

# 打印错误信息
if errors:
    print("处理过程中的错误:")
    for err in errors:
        print(f"  - {err}")

return f"销售总额:{total:.2f} 元"

# 先创建一个测试文件
with open("sales.csv", "w", encoding="utf-8") as f:
f.write("商品,数量,单价\n")
f.write("苹果,10,3.5\n")
f.write("香蕉,5,2.0\n")
f.write("西瓜,错误数据,8.0\n")  # 这行会出错

# 运行
print(calculate_sales("sales.csv"))

输出:

处理过程中的错误:
- 第3行:数据格式错误 could not convert string to float: '错误数据'
销售总额:45.00 元

一句话解释: 用嵌套的 try/except 处理行级别的错误,同时用外层 try/except 处理文件级别的错误。

配图2 - 配图2


项目 3:命令行待办清单(15 分钟)

一个带异常处理的待办清单工具,支持从文件读取、保存到文件。

import json
import os


TODO_FILE = "todos.json"

def load_todos():
"""加载待办清单"""
try:
    with open(TODO_FILE, "r", encoding="utf-8") as f:
        return json.load(f)
except FileNotFoundError:
    return []  # 文件不存在,返回空清单
except json.JSONDecodeError:
    print("警告:文件损坏,将创建新清单")
    return []

def save_todos(todos):
"""保存待办清单"""
try:
    with open(TODO_FILE, "w", encoding="utf-8") as f:
        json.dump(todos, f, ensure_ascii=False, indent=2)
except PermissionError:
    print("错误:没有写入权限")
    return False
return True

def add_todo(todos, task):
"""添加待办事项"""
if not task or not task.strip():
    raise ValueError("任务不能为空")
todos.append({"task": task.strip(), "done": False})
return todos

def complete_todo(todos, index):
"""标记完成"""
try:
    todos[index]["done"] = True
except IndexError:
    raise IndexError(f"任务 {index} 不存在")

def list_todos(todos):
"""显示待办清单"""
if not todos:
    print("清单是空的,添加一个吧!")
    return

for i, todo in enumerate(todos):
    status = "✓" if todo["done"] else "○"
    print(f"{i}. [{status}] {todo['task']}")

def main():
todos = load_todos()

print("=== 待办清单工具 ===")
print("命令:add <任务> / done <编号> / list / quit")

while True:
    try:
        cmd = input("\n请输入命令:").strip()
        if not cmd:
            continue

        if cmd == "quit":
            if save_todos(todos):
                print("再见!清单已保存")
            break
        elif cmd == "list":
            list_todos(todos)
        elif cmd.startswith("add "):
            task = cmd[4:]
            todos = add_todo(todos, task)
            print(f"已添加:{task}")
        elif cmd.startswith("done "):
            index = int(cmd[5:]) - 1  # 用户看到的是1开始的编号
            complete_todo(todos, index)
            print(f"已完成:{todos[index]['task']}")
        else:
            print("未知命令,有效命令:add, done, list, quit")

    except ValueError as e:
        print(f"输入错误:{e}")
    except IndexError as e:
        print(f"编号错误:{e}")

if __name__ == "__main__":
main()

示例运行:

=== 待办清单工具 ===
命令:add <任务> / done <编号> / list / quit

请输入命令:add 买牛奶
已添加:买牛奶

请输入命令:add 写周报
已添加:写周报

请输入命令:list
0. [○] 买牛奶
1. [○] 写周报

请输入命令:done 1
已完成:买牛奶

请输入命令:list
0. [✓] 买牛奶
1. [○] 写周报

请输入命令:quit
再见!清单已保存

一句话解释: 用异常处理保护文件读写和用户输入,让程序在各种出错情况下都能优雅应对。


💪 进阶:常见坑 + 小贴士

坑 1:裸 except 捕获所有异常

# ❌ 错误:这样会隐藏所有错误,包括 KeyboardInterrupt
try:
result = 10 / 0
except:
print("出错了")
# ✅ 正确:明确指定要捕获的异常类型
try:
result = 10 / 0
except ZeroDivisionError:
print("出错了")

坑 2:except 顺序写错

# ❌ 错误:父类在前,子类永远捕获不到
try:
result = 10 / 0
except Exception:
print("捕获了所有异常")
except ZeroDivisionError:
print("这段永远不会执行")
# ✅ 正确:子类在前,父类在后
try:
result = 10 / 0
except ZeroDivisionError:
print("专门处理除零")
except Exception:
print("处理其他异常")

坑 3:try/except/else/finally 混用时的执行顺序

try:
print("1. try 开始")
# raise ValueError("故意的")
print("2. try 结束")
except:
print("3. except 执行")
else:
print("4. else 执行(仅在try成功时)")
finally:
print("5. finally 总是执行")

正常情况输出:

1. try 开始
2. try 结束
4. else 执行(仅在try成功时)
5. finally 总是执行

有异常时输出:

1. try 开始
3. except 执行
5. finally 总是执行

坑 4:异常被吞掉

# ❌ 错误:捕获后什么都没做
try:
result = 10 / 0
except ZeroDivisionError:
pass  # 悄悄忽略错误
# ✅ 正确:至少记录日志
try:
result = 10 / 0
except ZeroDivisionError:
print("发生除零错误,已记录")
# 或者记录到日志文件

坑 5:文件操作没用 with

# ❌ 错误:忘记关闭文件
try:
file = open("data.txt", "r")
content = file.read()
except:
print("出错了")
# 如果这里出错,file 永远不会关闭
# ✅ 正确:用 with 自动管理
try:
with open("data.txt", "r") as file:
    content = file.read()
except:
print("出错了")
# with 会自动关闭文件,即使出错也不怕

调试技巧:traceback 模块

import traceback

try:
result = 10 / 0
except Exception:
# 打印完整的错误堆栈
traceback.print_exc()
# 或者获取字符串
error_info = traceback.format_exc()
print(f"错误详情:{error_info}")

✏️ 练习题

练习 1(2 分钟):抄改计算器
- 输入:safe_divide(20, 4)
- 预期输出:结果:5.0
- 提示:直接调用项目1的函数

练习 2(2 分钟):加个判断
- 在项目1的 safe_divide 中,如果结果大于10,返回 "结果大于10"
- 预期输出:safe_divide(15, 1) → "结果大于10"
- 提示:在 else 分支里加 if 判断

练习 3(3 分钟):读取新CSV
- 用项目2的方法处理以下数据:

名称,数量,单价
书本,3,45.5
铅笔,10,2.0
  • 预期输出:销售总额:156.50 元
  • 提示:创建新CSV文件,调用 calculate_sales

练习 4(3 分钟):串起两个项目
- 把项目2的CSV计算功能和项目3的保存功能结合
- 实现:从CSV读取数据,计算总额,保存结果到新文件
- 提示:两个函数的返回值都是字符串,可以直接写文件

练习 5(5 分钟):分析报错
- 运行以下代码,分析为什么会报错:

try:
data = {"name": "小明", "age": 20}
print(data["score"])
except KeyError:
print("KeyError")
except Exception:
print("Exception")
  • 预期输出:分析原因
  • 提示:看最后执行的是哪个 except

作业:做一个「数据验证工具」

做一个命令行工具,验证用户输入的数据是否符合规则:

  • 需求:提示用户输入姓名、年龄、邮箱,验证通过后保存到JSON文件
  • 功能点:
    1. 用 try/except 处理各种输入错误
    2. 用 raise 主动抛出验证失败
    3. 用 finally 确保程序结束时打印"验证完成"
    4. 数据保存用 JSON 格式
  • 加分项:
    1. 支持多次输入直到验证通过
    2. 把验证规则写成单独的函数
  • 验收标准:输入错误时提示友好,不崩溃;正确数据能保存到文件
  • 提交方式:评论区贴代码

📚 总结

这章学了3个核心点:
1. try/except 是程序的"安全带",让崩溃变成友好提示
2. else 处理成功情况,finally 处理清理工作
3. raise 可以主动告诉别人"这里有问题"

延伸学习:
- 官方文档:https://docs.python.org/3/tutorial/errors.html
- 书籍:《Python编程:从入门到实践》第9章
- 视频:B站「Python异常处理」相关教程

互动钩子: 你在写代码时遇到过什么奇葩崩溃?是用 try/except 解决的还是在评论区聊聊,老粉优先回复!

下一章我们要聊的是自定义异常——Python 内置的异常不够用时,怎么造自己的"专属异常"?敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。