2.2.2 异常处理

这一节让你的程序在出错时不至于直接崩溃。异常处理会在文件读写、网络请求、API 调用、数据清洗和模型推理中反复出现,重点是学会预判错误、捕获错误,并给出可恢复的处理方式。
- 理解什么是异常,为什么需要处理异常
- 掌握
try/except/else/finally的用法 - 学会捕获不同类型的异常
- 能编写健壮的、不会轻易崩溃的程序
什么是异常?
Section titled “什么是异常?”异常就是程序运行时发生的错误。没有异常处理的程序,一遇到错误就会直接崩溃:
# 这些代码都会导致程序崩溃print(10 / 0) # ZeroDivisionError: 除以零print(int("abc")) # ValueError: 无法转换print([1, 2, 3][10]) # IndexError: 索引越界print({"a": 1}["b"]) # KeyError: 键不存在
# 程序崩溃意味着后面的代码都不会执行print("这行永远不会被执行")在真实的程序中,错误是不可避免的——用户可能输入非法数据、文件可能不存在、网络可能断开。异常处理让你能优雅地应对这些问题,而不是让程序直接崩溃。
学完这一页,至少保留这张证据卡:
- 模式
- 类、异常、文件 IO、函数式流水线、生成器或类型提示
- 代码产物
- 最小可运行示例和一个真实使用场景
- 输出
- 打印的对象状态、捕获的错误、保存的文件、yield 的值,或类型检查备注
- 失败检查
- 隐藏变异、吞掉异常、文件路径问题、懒迭代器混淆或误导性标注
- 期望产出
- 带调试说明的小型高级 Python 示例
常见的异常类型
Section titled “常见的异常类型”| 异常类型 | 触发场景 | 示例 |
|---|---|---|
ZeroDivisionError | 除以零 | 1 / 0 |
TypeError | 类型操作不匹配 | "hello" + 5 |
ValueError | 值不合法 | int("abc") |
IndexError | 列表索引越界 | [1, 2][5] |
KeyError | 字典键不存在 | {"a": 1}["b"] |
FileNotFoundError | 文件不存在 | open("不存在.txt") |
AttributeError | 属性不存在 | "hello".foo() |
NameError | 变量未定义 | print(xyz) |
ImportError | 导入失败 | import 不存在的模块 |
try / except 基本用法
Section titled “try / except 基本用法”try/except 的逻辑是:尝试执行代码,如果出错了,执行备选方案。
try: number = int(input("请输入一个数字: ")) print(f"你输入的是: {number}")except ValueError: print("输入无效!请输入一个数字。")
print("程序继续运行...") # 不管有没有异常,这行都会执行运行效果:
| 输入 | 会发生什么 |
|---|---|
42 | 打印 你输入的是: 42,然后继续运行。 |
abc | 打印输入无效提示,然后继续运行。 |
关键点:有了 try/except,程序不会因为错误而崩溃。
捕获不同类型的异常
Section titled “捕获不同类型的异常”捕获多种异常
Section titled “捕获多种异常”def safe_divide(a, b): try: result = a / b return result except ZeroDivisionError: print("错误:不能除以零!") return None except TypeError: print("错误:请传入数字!") return None
print(safe_divide(10, 3)) # 3.333...print(safe_divide(10, 0)) # 错误:不能除以零! → Noneprint(safe_divide("10", 3)) # 错误:请传入数字! → None捕获多种异常(合并写法)
Section titled “捕获多种异常(合并写法)”try: # 可能出错的代码 value = int(input("请输入数字: ")) result = 100 / value print(f"结果: {result}")except (ValueError, ZeroDivisionError) as e: print(f"出错了: {e}")获取异常信息
Section titled “获取异常信息”try: number = int("abc")except ValueError as e: print(f"异常类型: {type(e).__name__}") # ValueError print(f"异常信息: {e}") # invalid literal for int() with base 10: 'abc'捕获所有异常(谨慎使用)
Section titled “捕获所有异常(谨慎使用)”try: # 一些代码 result = risky_operation()except Exception as e: print(f"发生了意外错误: {type(e).__name__}: {e}")try / except / else / finally
Section titled “try / except / else / finally”完整的异常处理结构有四个部分:
try: # 尝试执行的代码 file = open("data.txt", "r") content = file.read()except FileNotFoundError: # 出错时执行 print("文件不存在!")else: # 没有出错时执行 print(f"文件内容: {content}")finally: # 不管有没有出错都执行(通常用来清理资源) print("操作完成")| 子句 | 何时执行 | 用途 |
|---|---|---|
try | 总是执行 | 放可能出错的代码 |
except | 只在出错时执行 | 处理错误 |
else | 只在没出错时执行 | 放成功后的逻辑 |
finally | 不管有没有出错都执行 | 清理资源(关闭文件、断开连接) |
finally 的典型用途
Section titled “finally 的典型用途”file = Nonetry: file = open("data.txt", "r") data = file.read() # 处理数据...except FileNotFoundError: print("文件不存在")finally: if file: file.close() # 不管有没有出错,都要关闭文件 print("文件已关闭")除了处理异常,你也可以主动抛出异常——当你发现一个不合理的状态时,告诉调用者”出问题了”。
raise 语句
Section titled “raise 语句”def set_age(age): if not isinstance(age, int): raise TypeError("年龄必须是整数") if age < 0 or age > 150: raise ValueError(f"年龄 {age} 不合理,应该在 0-150 之间") return age
# 正常使用print(set_age(25)) # 25
# 触发异常try: set_age(-5)except ValueError as e: print(f"错误: {e}") # 错误: 年龄 -5 不合理,应该在 0-150 之间
try: set_age("二十")except TypeError as e: print(f"错误: {e}") # 错误: 年龄必须是整数当内置异常类型不够用时,可以自定义:
class InsufficientFundsError(Exception): """余额不足异常""" def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__(f"余额不足:当前余额 {balance},尝试取出 {amount}")
class BankAccount: def __init__(self, balance=0): self.balance = balance
def withdraw(self, amount): if amount > self.balance: raise InsufficientFundsError(self.balance, amount) self.balance -= amount return self.balance
# 使用account = BankAccount(1000)try: account.withdraw(1500)except InsufficientFundsError as e: print(f"交易失败: {e}") print(f"当前余额: {e.balance}, 请求金额: {e.amount}")模式 1:LBYL vs EAFP
Section titled “模式 1:LBYL vs EAFP”Python 社区推崇 EAFP(Easier to Ask Forgiveness than Permission,先做再说)而不是 LBYL(Look Before You Leap,先检查再做):
# LBYL 风格(先检查再操作)—— 不够 Pythonicif key in my_dict: value = my_dict[key]else: value = default_value
# EAFP 风格(先操作,出错再处理)—— 更 Pythonictry: value = my_dict[key]except KeyError: value = default_value
# 当然,字典有更好的写法value = my_dict.get(key, default_value)模式 2:重试机制
Section titled “模式 2:重试机制”import time
def fetch_data_with_retry(url, max_retries=3): """带重试的数据获取""" for attempt in range(1, max_retries + 1): try: print(f"第 {attempt} 次尝试...") # 模拟网络请求 import random if random.random() < 0.5: raise ConnectionError("网络连接失败") return "获取到的数据" except ConnectionError as e: print(f" 失败: {e}") if attempt < max_retries: wait = attempt * 2 # 递增等待时间 print(f" {wait} 秒后重试...") time.sleep(wait) else: print(" 所有重试均失败!") raise # 最后一次重试失败,抛出异常
try: data = fetch_data_with_retry("https://api.example.com") print(f"成功: {data}")except ConnectionError: print("最终获取数据失败")模式 3:安全的用户输入
Section titled “模式 3:安全的用户输入”def get_number(prompt, min_val=None, max_val=None): """安全地获取用户输入的数字""" while True: try: value = float(input(prompt)) if min_val is not None and value < min_val: print(f"请输入不小于 {min_val} 的数") continue if max_val is not None and value > max_val: print(f"请输入不大于 {max_val} 的数") continue return value except ValueError: print("请输入有效的数字!")
# 使用age = get_number("请输入年龄: ", min_val=0, max_val=150)print(f"你的年龄是: {age}")综合案例:安全的任务估算管理器
Section titled “综合案例:安全的任务估算管理器”class TaskEstimateManager: def __init__(self): self.tasks = {}
def add_task(self, name, hours): """添加任务估算""" if not isinstance(name, str) or not name.strip(): raise ValueError("任务名称不能为空") if not isinstance(hours, (int, float)): raise TypeError(f"工时必须是数字,收到: {type(hours).__name__}") if not 0 <= hours <= 80: raise ValueError(f"工时 {hours} 超出范围(0-80)")
self.tasks[name] = hours print(f"✅ 添加成功: {name} - {hours} 小时")
def get_average_hours(self): """获取平均估算""" if not self.tasks: raise RuntimeError("没有任务数据,无法计算平均值") return sum(self.tasks.values()) / len(self.tasks)
def get_task(self, name): """查询任务估算""" if name not in self.tasks: raise KeyError(f"找不到任务: {name}") return self.tasks[name]
# 使用manager = TaskEstimateManager()
# 安全地添加任务估算test_data = [ ("登录 API", 8), ("RAG 演示", 12), ("图表视图", "尽快"), # 类型错误 ("数据导入", 120), # 范围错误 ("", 6), # 名称为空 ("部署脚本", 5),]
for name, hours in test_data: try: manager.add_task(name, hours) except (ValueError, TypeError) as e: print(f"❌ 添加失败: {e}")
# 查询print(f"\n平均估算: {manager.get_average_hours():.1f} 小时")
try: print(manager.get_task("支付流程"))except KeyError as e: print(f"查询失败: {e}")练习 1:安全的计算器
Section titled “练习 1:安全的计算器”def safe_calculator(inputs=None): """安全的四则运算器,能处理非法输入和除以零。""" inputs = iter(inputs or ["10", "0", "/", "n"])
while True: try: a = float(next(inputs) if inputs else input("第一个数字:")) b = float(next(inputs) if inputs else input("第二个数字:")) op = next(inputs) if inputs else input("运算符(+、-、*、/):")
if op == "+": result = a + b elif op == "-": result = a - b elif op == "*": result = a * b elif op == "/": result = a / b else: raise ValueError(f"不支持的运算符:{op}")
print(f"结果:{result}") except ZeroDivisionError: print("不能除以零。") except ValueError as error: print(f"输入不合法:{error}") except StopIteration: break
again = next(inputs, "n") if inputs else input("是否继续?(y/n):") if again.lower() != "y": break
safe_calculator()练习 2:文件读取器
Section titled “练习 2:文件读取器”def read_file_safely(filename): """安全地读取文件内容。""" try: with open(filename, "r", encoding="utf-8") as file: return file.read() except FileNotFoundError: print(f"文件不存在:{filename}") except PermissionError: print(f"没有权限读取:{filename}") except OSError as error: print(f"读取失败:{error}") return None
content = read_file_safely("test.txt")if content: print(content)练习 3:批量类型转换
Section titled “练习 3:批量类型转换”def convert_to_numbers(data_list): """把字符串转换成数字,同时保留失败原因。""" numbers = [] errors = [] for item in data_list: try: numbers.append(float(item)) except ValueError: numbers.append(None) errors.append(f"{item} 无法转换") return numbers, errors
values, errors = convert_to_numbers(["10", "20.5", "abc", "30", "xyz"])print(values)print(errors)参考实现与讲解
safe_calculator应先把输入转换成数字,再按运算符分支,并捕获ZeroDivisionError、ValueError和StopIteration。使用默认输入时,它会先走到除零分支,打印友好错误,然后在最后的n处退出。read_file_safely应使用with语句,捕获FileNotFoundError、PermissionError和其他OSError,在读取失败时返回None,让调用方决定下一步。convert_to_numbers应返回两列平行列表:解析成功的数字和转换失败记录。把None放进数字列表可以保持批次对齐,同时保留坏记录。
| 语法 | 作用 | 何时使用 |
|---|---|---|
try | 包裹可能出错的代码 | 任何可能出错的地方 |
except | 捕获并处理异常 | 指定要处理的异常类型 |
else | 没有异常时执行 | 成功后的逻辑 |
finally | 始终执行 | 清理资源 |
raise | 主动抛出异常 | 输入不合法、状态不对时 |
| 自定义异常 | 创建业务相关的异常 | 内置异常不够描述性时 |