跳转到内容

2.2.2 异常处理

异常处理执行流程图

这一节让你的程序在出错时不至于直接崩溃。异常处理会在文件读写、网络请求、API 调用、数据清洗和模型推理中反复出现,重点是学会预判错误、捕获错误,并给出可恢复的处理方式。

  • 理解什么是异常,为什么需要处理异常
  • 掌握 try/except/else/finally 的用法
  • 学会捕获不同类型的异常
  • 能编写健壮的、不会轻易崩溃的程序

异常就是程序运行时发生的错误。没有异常处理的程序,一遇到错误就会直接崩溃:

# 这些代码都会导致程序崩溃
print(10 / 0) # ZeroDivisionError: 除以零
print(int("abc")) # ValueError: 无法转换
print([1, 2, 3][10]) # IndexError: 索引越界
print({"a": 1}["b"]) # KeyError: 键不存在
# 程序崩溃意味着后面的代码都不会执行
print("这行永远不会被执行")

在真实的程序中,错误是不可避免的——用户可能输入非法数据、文件可能不存在、网络可能断开。异常处理让你能优雅地应对这些问题,而不是让程序直接崩溃。


学完这一页,至少保留这张证据卡:

模式
类、异常、文件 IO、函数式流水线、生成器或类型提示
代码产物
最小可运行示例和一个真实使用场景
输出
打印的对象状态、捕获的错误、保存的文件、yield 的值,或类型检查备注
失败检查
隐藏变异、吞掉异常、文件路径问题、懒迭代器混淆或误导性标注
期望产出
带调试说明的小型高级 Python 示例
异常类型触发场景示例
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 的逻辑是:尝试执行代码,如果出错了,执行备选方案。

try:
number = int(input("请输入一个数字: "))
print(f"你输入的是: {number}")
except ValueError:
print("输入无效!请输入一个数字。")
print("程序继续运行...") # 不管有没有异常,这行都会执行

运行效果:

输入会发生什么
42打印 你输入的是: 42,然后继续运行。
abc打印输入无效提示,然后继续运行。

关键点:有了 try/except,程序不会因为错误而崩溃。


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)) # 错误:不能除以零! → None
print(safe_divide("10", 3)) # 错误:请传入数字! → None
try:
# 可能出错的代码
value = int(input("请输入数字: "))
result = 100 / value
print(f"结果: {result}")
except (ValueError, ZeroDivisionError) as e:
print(f"出错了: {e}")
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'
try:
# 一些代码
result = risky_operation()
except Exception as e:
print(f"发生了意外错误: {type(e).__name__}: {e}")

完整的异常处理结构有四个部分:

try:
# 尝试执行的代码
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
# 出错时执行
print("文件不存在!")
else:
# 没有出错时执行
print(f"文件内容: {content}")
finally:
# 不管有没有出错都执行(通常用来清理资源)
print("操作完成")
子句何时执行用途
try总是执行放可能出错的代码
except只在出错时执行处理错误
else只在没出错时执行放成功后的逻辑
finally不管有没有出错都执行清理资源(关闭文件、断开连接)
file = None
try:
file = open("data.txt", "r")
data = file.read()
# 处理数据...
except FileNotFoundError:
print("文件不存在")
finally:
if file:
file.close() # 不管有没有出错,都要关闭文件
print("文件已关闭")

除了处理异常,你也可以主动抛出异常——当你发现一个不合理的状态时,告诉调用者”出问题了”。

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}")

Python 社区推崇 EAFP(Easier to Ask Forgiveness than Permission,先做再说)而不是 LBYL(Look Before You Leap,先检查再做):

# LBYL 风格(先检查再操作)—— 不够 Pythonic
if key in my_dict:
value = my_dict[key]
else:
value = default_value
# EAFP 风格(先操作,出错再处理)—— 更 Pythonic
try:
value = my_dict[key]
except KeyError:
value = default_value
# 当然,字典有更好的写法
value = my_dict.get(key, default_value)
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("最终获取数据失败")
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}")

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()
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)
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)
参考实现与讲解
  1. safe_calculator 应先把输入转换成数字,再按运算符分支,并捕获 ZeroDivisionErrorValueErrorStopIteration。使用默认输入时,它会先走到除零分支,打印友好错误,然后在最后的 n 处退出。
  2. read_file_safely 应使用 with 语句,捕获 FileNotFoundErrorPermissionError 和其他 OSError,在读取失败时返回 None,让调用方决定下一步。
  3. convert_to_numbers 应返回两列平行列表:解析成功的数字和转换失败记录。把 None 放进数字列表可以保持批次对齐,同时保留坏记录。

语法作用何时使用
try包裹可能出错的代码任何可能出错的地方
except捕获并处理异常指定要处理的异常类型
else没有异常时执行成功后的逻辑
finally始终执行清理资源
raise主动抛出异常输入不合法、状态不对时
自定义异常创建业务相关的异常内置异常不够描述性时