详解try/except避坑指南,看完再也不怕程序崩溃
你正在开发一个扫雷游戏,玩家点击格子时,你信心满满地写下了这样的代码:
defclick_cell(x, y):
board[x][y].revealed = True
if board[x][y].has_mine:
game_over()
然后测试的时候,玩家win+Tab切出去回个消息,回来点击格子——程序原地爆炸
你看着控制台满屏的IndexError,陷入了沉思:明明本地测试得好好的,怎么换个环境就崩了
恭喜你,你踩到了Python异常处理的第一个坑
作为一个写代码经常翻车的老玩家,今天我把异常处理中最常见的5个坑全部整理出来了
第3个坑连工作好几年的老手都会翻车,建议先收藏再看
第一个坑:裸奔的try-except - 你的程序正在"裸泳"
场景重现:猜数字游戏的诡异崩溃
小甲鱼写了这样一个猜数字游戏:
import random
defguess_number():
target = random.randint(1, 100)
print(f"我想了一个1-100之间的数字,你猜是几?")
whileTrue:
try:
guess = int(input("请输入你的猜测: "))
if guess == target:
print("恭喜你,猜对了!")
break
elif guess < target:
print("太小了!")
else:
print("太大了!")
except:
print("请输入有效的数字!")
看起来很完美对不对
用户输入非数字时,会打印"请输入有效的数字
"
**但实际上,这个程序有个致命问题——它在偷偷吞掉所有异常
**
当程序出现任何其他问题时,比如你后续加了一个计分功能:
defadd_score(points):
score = get_score() # 假设这个函数有问题
score += points
如果get_score()抛出AttributeError,这个异常会被上面的裸except吞掉,用户只会看到"请输入有效的数字
",而实际上问题完全无关输入
问题分析
裸奔的except(即except:不指定异常类型)相当于给程序穿了一件透明雨衣——看起来在保护它,其实完全没用
它会捕获所有异常,包括KeyboardInterrupt(用户按Ctrl+C)、SystemExit(程序退出),甚至MemoryError(内存不足)
解决方案
永远指定具体的异常类型:
try:
guess = int(input("请输入你的猜测: "))
except ValueError:
print("请输入有效的数字!")
except KeyboardInterrupt:
print("\n游戏被中断,再见!")
break
完整代码示例
import random
defguess_number():
target = random.randint(1, 100)
attempts = 0
print("=" * 40)
print("🎮 猜数字游戏 - 异常处理教学版")
print("我想了一个1-100之间的数字,你猜是几?")
print("输入 'q' 退出游戏")
print("=" * 40)
whileTrue:
try:
user_input = input("请输入你的猜测: ")
# 处理退出
if user_input.lower() == 'q':
print(f"游戏结束!你总共猜了{attempts}次")
break
# 只捕获ValueError - 输入解析错误
guess = int(user_input)
attempts += 1
if guess == target:
print(f"🎉 恭喜你,猜对了!你总共用了{attempts}次机会")
break
elif guess < target:
print("📉 太小了!继续猜")
else:
print("📈 太大了!继续猜")
except ValueError:
print("❌ 输入错误!请输入一个整数(或输入'q'退出)")
except KeyboardInterrupt:
print("\n👋 游戏被中断,再见!")
break
if __name__ == "__main__":
guess_number()
避坑要点:只捕获你知道的、能够处理的异常类型
不要用裸except,它会让你的调试变成噩梦
第二个坑:except Exception - 你在用渔网捞鱼
场景重现:计算器的隐藏炸弹
继续说小甲鱼的计算器项目
这次他学聪明了,不用裸奔的except,而是用了Exception:
defcalculator():
whileTrue:
try:
expression = input("请输入计算式 (如 2+3): ")
result = eval(expression)
print(f"结果是: {result}")
except Exception as e:
print(f"计算出错: {e}")
看起来很棒,还能打印错误信息帮助用户
但某天,用户输入了import os; os.system('rm -rf /')……
虽然eval本身就很危险,但这里我们要说的是另一个问题:你在用渔网捞鱼的时候,把大鲨鱼也捞进来了
问题分析
Exception是所有非系统退出的异常的基类
用except Exception相当于说"我想捕获所有可能出错的情况"——但这通常是懒惰的做法
问题在于:
- 2. 某些异常本不应该被捕获(比如
StopIteration在生成器中使用时) - 3. 代码的可读性极差,后续维护的人不知道你会处理什么
解决方案
按具体异常类型分层捕获:
defsafe_calculator():
whileTrue:
try:
expression = input("请输入计算式 (如 2+3): ")
result = eval(expression)
print(f"结果是: {result}")
except ZeroDivisionError:
print("❌ 除数不能为零!")
except SyntaxError:
print("❌ 表达式语法错误,请检查输入格式")
except (TypeError, NameError) as e:
print(f"❌ 计算错误: {str(e)}")
except KeyboardInterrupt:
print("\n👋 计算器已退出")
break
完整代码示例
defcalculator():
"""带详细异常处理的计算器"""
print("=" * 50)
print("🧮 智能计算器 - 异常处理进阶版")
print("支持 + - * / ** % // 操作")
print("输入 'help' 查看帮助,'quit' 退出")
print("=" * 50)
whileTrue:
try:
user_input = input("\n请输入计算式: ").strip()
if user_input.lower() in ('quit', 'q', 'exit'):
print("👋 计算器已退出,感谢使用!")
break
if user_input.lower() == 'help':
print_help()
continue
# 检查输入是否为空
ifnot user_input:
print("⚠️ 请输入有效的计算式")
continue
result = eval(user_input)
# 检查结果是否合法
ifisinstance(result, complex):
print("⚠️ 不支持复数运算")
elif result == float('inf') or result == float('-inf'):
print("⚠️ 结果超出可表示范围")
else:
print(f"✅ 计算结果: {result}")
except ZeroDivisionError:
print("❌ 错误: 除数不能为零!")
except SyntaxError:
print("❌ 语法错误: 请检查表达式格式")
print(" 正确示例: 2 + 3 * 4")
except TypeError as e:
print(f"❌ 类型错误: {str(e)}")
print(" 提示: 不能对字符串和数字直接运算")
except NameError as e:
print(f"❌ 变量错误: {str(e)}")
print(" 提示: 表达式中不能包含未定义的变量")
except KeyboardInterrupt:
print("\n👋 程序被中断,退出计算器")
break
except Exception as e:
# 这是一个兜底catch,不应该经常触发
print(f"❓ 未知错误: {type(e).__name__}: {str(e)}")
print(" 如果持续出现此问题,请联系开发者")
defprint_help():
print("""
📖 计算器帮助:
- 加法: 2 + 3
- 减法: 10 - 4
- 乘法: 5 * 6
- 除法: 8 / 2
- 整除: 10 // 3
- 取余: 10 % 3
- 幂运算: 2 ** 10
""")
if __name__ == "__main__":
calculator()
避坑要点:使用Exception作为兜底可以,但在此之前先处理具体的异常类型
这不仅更安全,代码可读性也更好
第三个坑:except不指定异常对象 - 老手也翻车的魔咒
场景重现:扫雷游戏的神秘崩溃
这是本文最核心的坑,也是我见过最多人翻车的
小甲鱼在开发扫雷游戏时,写了这样的代码:
defreveal_cell(x, y):
try:
cell = board[x][y]
if cell.is_mine:
explode()
else:
count_mines = count_adjacent_mines(x, y)
cell.reveal(count_mines)
except IndexError:
print("超出边界")
看起来很标准
捕获IndexError,处理边界问题
**但某天,玩家在边缘点击时,游戏没有提示"超出边界",而是直接闪退了
**
问题分析
等等,我刚才的代码不是已经捕获了IndexError吗
让我再仔细看看:
except IndexError:
print("超出边界")
**你没有把异常对象赋值给变量
**
虽然这行代码能正常捕获IndexError,但问题在于——如果你在try块里抛出的是其他异常呢
比如:
defreveal_cell(x, y):
try:
cell = board[x][y] # 抛出 IndexError
if cell.is_mine: # 抛出 AttributeError
explode()
else:
count_mines = count_adjacent_mines(x, y)
cell.reveal(count_mines)
except IndexError:
print("超出边界")
当玩家点击一个有问题的格子(比如cell对象没有正确初始化),代码会抛出AttributeError,但你的except只处理了IndexError
结果就是AttributeError没有被捕获,程序直接崩溃
解决方案
一定要指定异常对象,并且先处理最具体的异常:
defreveal_cell(x, y):
try:
cell = board[x][y]
if cell.is_mine:
explode()
else:
count_mines = count_adjacent_mines(x, y)
cell.reveal(count_mines)
except IndexError as e:
print(f"超出边界: {e}")
except AttributeError as e:
print(f"格子数据错误: {e}")
# 这里应该记录日志并修复数据
except Exception as e:
print(f"未知错误: {type(e).__name__}: {e}")
raise# 重新抛出,让上层处理
完整代码示例
classCell:
"""扫雷游戏中的格子"""
def__init__(self, x, y, has_mine=False):
self.x = x
self.y = y
self.has_mine = has_mine
self.revealed = False
self._mine_count = None
@property
defis_mine(self):
"""安全访问mine属性"""
ifnothasattr(self, '_has_mine'):
returnFalse
returnself._has_mine
@is_mine.setter
defis_mine(self, value):
self._has_mine = value
defreveal(self, mine_count=0):
ifself.revealed:
returnFalse
self.revealed = True
self._mine_count = mine_count
returnTrue
def__str__(self):
ifnotself.revealed:
return"⬜"
ifself.has_mine:
return"💣"
ifself._mine_count == 0:
return"📂"
returnstr(self._mine_count)
classMinesweeper:
"""扫雷游戏主类 - 展示正确的异常处理"""
def__init__(self, width=8, height=8, mines=10):
self.width = width
self.height = height
self.mines = mines
self.board = [[Cell(x, y) for y inrange(height)] for x inrange(width)]
self.game_over = False
self._place_mines()
def_place_mines(self):
"""随机放置地雷"""
import random
positions = [(x, y) for x inrange(self.width) for y inrange(self.height)]
mine_positions = random.sample(positions, self.mines)
for x, y in mine_positions:
self.board[x][y].has_mine = True
defcount_adjacent_mines(self, x, y):
"""计算周围地雷数量"""
count = 0
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if dx == 0and dy == 0:
continue
try:
ifself.board[x + dx][y + dy].has_mine:
count += 1
except IndexError:
# 边缘情况,忽略
pass
return count
defreveal_cell(self, x, y):
"""点击格子 - 展示正确的异常处理"""
try:
# 第一个可能出错的点:数组越界
cell = self.board[x][y]
# 第二个可能出错的点:属性访问
if cell.has_mine:
self.game_over = True
print("\n💥 BOOM!你踩到地雷了!")
self.print_board(reveal_all=True)
return
# 第三个可能出错的点:方法调用
mine_count = self.count_adjacent_mines(x, y)
cell.reveal(mine_count)
# 检查是否赢了
self._check_win()
except IndexError as e:
print(f"❌ 坐标超出范围: ({x}, {y})")
print(f" 有效范围: x=[0, {self.width-1}], y=[0, {self.height-1}]")
except AttributeError as e:
print(f"❌ 格子数据异常: {str(e)}")
print(" 正在尝试修复...")
self.board[x][y] = Cell(x, y, False)
except Exception as e:
print(f"❓ 未知错误: {type(e).__name__}: {str(e)}")
raise# 重新抛出异常,让调用者知道
def_check_win(self):
"""检查是否获胜"""
revealed_count = sum(
cell.revealed
for row inself.board
for cell in row
)
total_cells = self.width * self.height
if revealed_count == total_cells - self.mines:
self.game_over = True
print("\n🎉 恭喜你!你赢了!")
self.print_board(reveal_all=True)
defprint_board(self, reveal_all=False):
"""打印棋盘"""
print("\n " + " ".join(str(i) for i inrange(self.height)))
print(" " + "-" * (self.height * 2 + 1))
for x inrange(self.width):
print(f"{x}| ", end="")
for y inrange(self.height):
cell = self.board[x][y]
if reveal_all or cell.revealed:
print(cell, end=" ")
else:
print("⬜", end=" ")
print()
print()
defplay_minesweeper():
"""扫雷游戏主循环"""
game = Minesweeper(8, 8, 10)
print("=" * 50)
print("💣 扫雷游戏 - 异常处理终极教学版")
print("输入坐标进行游戏,格式: x y")
print("输入 'q' 退出游戏")
print("=" * 50)
game.print_board()
whilenot game.game_over:
try:
user_input = input("请输入坐标 (如 3 4): ").strip()
if user_input.lower() in ('q', 'quit', 'exit'):
print("👋 游戏已退出")
break
# 解析输入
parts = user_input.split()
iflen(parts) != 2:
print("⚠️ 请输入两个数字,用空格分隔")
continue
x = int(parts[0])
y = int(parts[1])
game.reveal_cell(x, y)
ifnot game.game_over:
game.print_board()
except ValueError:
print("❌ 请输入有效的数字")
except KeyboardInterrupt:
print("\n👋 游戏被中断")
break
if __name__ == "__main__":
play_minesweeper()
避坑要点:这个坑的可怕之处在于——它不会每次都触发
代码在大多数情况下运行正常,只有特定边界条件下才会崩溃
所以很多老手也意识不到自己翻车了
记住:**永远用except Exception as e:,永远先处理具体异常,再处理通用异常
**
第四个坑:finally块中的危险操作 - 温柔陷阱
场景重现:游戏存档的诡异丢失
小甲鱼做了个游戏存档系统,用finally确保文件关闭:
defsave_game(filename):
file = open(filename, 'w')
try:
# 保存游戏数据
data = get_game_data()
file.write(str(data))
finally:
file.close()
看起来很完美
无论成功还是失败,文件都会关闭
**但如果get_game_data()抛出了异常呢
**
defsave_game(filename):
file = open(filename, 'w')
try:
data = get_game_data() # 这里抛出异常!
file.write(str(data))
finally:
file.close() # 会执行,但数据没保存
程序崩溃了,但finally块仍然执行了,文件被关闭了,但数据没有保存成功
用户丢失了游戏进度
问题分析
finally块的特性是:无论try块中发生什么,它一定会执行
这既是优点也是陷阱
常见错误:
- 1. 在
finally中做关键操作(如保存数据、发送请求)
解决方案
关键操作放在try块中,使用上下文管理器:
defsave_game(filename):
# 使用with语句,确保文件正确关闭
withopen(filename, 'w') as file:
# 保存游戏数据
data = get_game_data()
file.write(str(data))
# 如果到这里,说明保存成功
returnTrue
如果要处理异常:
defsave_game(filename):
try:
withopen(filename, 'w') as file:
data = get_game_data()
file.write(str(data))
returnTrue
except IOError as e:
print(f"保存失败: {e}")
returnFalse
except Exception as e:
print(f"未知错误: {e}")
returnFalse
完整代码示例
import json
import time
classGameSaveSystem:
"""游戏存档系统 - 展示finally的正确用法"""
def__init__(self):
self.save_slots = 3
self.saves = {}
defsave_game(self, slot, game_data):
"""保存游戏 - 正确版本"""
filename = f"save_slot_{slot}.json"
# 验证存档槽位
if slot < 1or slot > self.save_slots:
raise ValueError(f"无效的存档槽位: {slot} (有效范围: 1-{self.save_slots})")
# 使用with语句,文件操作在try块内
try:
withopen(filename, 'w', encoding='utf-8') as f:
# 准备存档数据
save_data = {
'timestamp': time.time(),
'game_state': game_data,
'version': '1.0.0'
}
# 写入数据
json.dump(save_data, f, ensure_ascii=False, indent=2)
# 只有成功写入才更新内存记录
self.saves[slot] = save_data
print(f"✅ 游戏已保存到槽位 {slot}")
returnTrue
except PermissionError:
print(f"❌ 保存失败: 没有写入权限")
returnFalse
except IOError as e:
print(f"❌ 保存失败: {str(e)}")
returnFalse
except Exception as e:
print(f"❌ 保存失败: {type(e).__name__}: {str(e)}")
returnFalse
defload_game(self, slot):
"""加载游戏"""
filename = f"save_slot_{slot}.json"
try:
withopen(filename, 'r', encoding='utf-8') as f:
save_data = json.load(f)
# 验证存档版本
if save_data.get('version') != '1.0.0':
print("⚠️ 存档版本不匹配,可能存在兼容性问题")
print(f"✅ 成功从槽位 {slot} 加载游戏")
return save_data['game_state']
except FileNotFoundError:
print(f"❌ 槽位 {slot} 没有存档")
returnNone
except json.JSONDecodeError as e:
print(f"❌ 存档文件损坏: {str(e)}")
returnNone
except Exception as e:
print(f"❌ 加载失败: {type(e).__name__}: {str(e)}")
returnNone
defdelete_save(self, slot):
"""删除存档"""
import os
filename = f"save_slot_{slot}.json"
try:
if os.path.exists(filename):
os.remove(filename)
if slot inself.saves:
delself.saves[slot]
print(f"🗑️ 槽位 {slot} 的存档已删除")
else:
print(f"槽位 {slot} 没有存档")
except Exception as e:
print(f"❌ 删除失败: {str(e)}")
defdemo_save_system():
"""演示游戏存档系统"""
system = GameSaveSystem()
print("=" * 50)
print("💾 游戏存档系统 - finally的正确用法")
print("=" * 50)
# 模拟游戏数据
sample_game_state = {
'player': {'name': 'Hero', 'level': 10, 'hp': 100},
'position': {'x': 50, 'y': 75},
'inventory': ['sword', 'shield', 'potion']
}
# 保存游戏
print("\n--- 保存游戏 ---")
system.save_game(1, sample_game_state)
# 尝试加载
print("\n--- 加载游戏 ---")
loaded_data = system.load_game(1)
if loaded_data:
print(f"玩家: {loaded_data['player']['name']}")
print(f"等级: {loaded_data['player']['level']}")
# 测试异常情况
print("\n--- 测试异常处理 ---")
system.save_game(99, sample_game_data := {}) # 无效槽位
# 测试finally(通过异常)
print("\n--- finally的正确用法演示 ---")
try:
withopen("temp.txt", "w") as f:
f.write("test")
raise RuntimeError("模拟异常")
except RuntimeError:
print("异常被捕获,文件已正确关闭")
finally:
print("finally块执行 - 这里做清理工作,不要做关键操作")
if __name__ == "__main__":
demo_save_system()
避坑要点:finally块适合做清理工作(关闭文件、释放资源、断开连接),但不要在这里做关键操作(保存数据、发送请求)
记住:finally执行时,try块中的操作可能已经失败了
第五个坑:自定义异常的滥用 - 过度设计
场景重现:三层嵌套的自定义异常
小甲鱼决定要让自己的代码"更专业",于是定义了层层嵌套的自定义异常:
classGameError(Exception):
"""游戏基础异常"""
pass
classPlayerError(GameError):
"""玩家相关错误"""
pass
classPlayerNotFoundError(PlayerError):
"""玩家不存在"""
pass
classPlayerLevelTooLowError(PlayerError):
"""玩家等级太低"""
pass
classPlayerInventoryFullError(PlayerError):
"""背包满了"""
pass
# ... 还有20多个
然后使用的时候:
try:
player = find_player(name)
player.equip(item)
except PlayerNotFoundError:
print("玩家不存在")
except PlayerLevelTooLowError:
print("等级不够")
except PlayerInventoryFullError:
print("背包满了")
except PlayerError:
print("玩家相关错误")
except GameError:
print("游戏错误")
except Exception:
print("未知错误")
**三层嵌套之后,维护者(包括小甲鱼自己)已经完全不知道该怎么捕获异常了
**
问题分析
过度设计自定义异常的问题:
解决方案
原则:内置异常优先,自定义异常用在特定领域:
# 简单情况:用内置异常
if player isNone:
raise ValueError(f"玩家不存在: {name}")
if player.level < required_level:
raise ValueError(f"玩家等级不足: 需要{required_level}级,当前{player.level}级")
# 特定领域:用自定义异常
classGameError(Exception):
"""游戏业务相关的异常基类"""
pass
classSaveGameError(GameError):
"""存档相关错误"""
pass
完整代码示例
# 正确的自定义异常使用方式
# 1. 只定义业务相关的异常基类
classGameError(Exception):
"""游戏基础异常,所有游戏相关异常的父类"""
def__init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code
# 2. 有限的具体异常(根据业务需求)
classSaveGameError(GameError):
"""存档操作失败"""
pass
classLoadGameError(GameError):
"""加载存档失败"""
pass
classGameLogicError(GameError):
"""游戏逻辑错误"""
pass
# 3. 游戏引擎示例
classPlayer:
"""玩家类 - 展示正确的异常使用"""
def__init__(self, name, level=1):
self.name = name
self.level = level
self._inventory = []
self._max_inventory = 20
defadd_item(self, item):
"""添加物品到背包"""
iflen(self._inventory) >= self._max_inventory:
# 使用ValueError,比自定义异常更清晰
raise ValueError(f"背包已满!当前: {len(self._inventory)}, 最大: {self._max_inventory}")
ifself.level < item.required_level:
raise GameLogicError(
f"等级不足: 需要{item.required_level}级,当前{self.level}级",
error_code="LEVEL_TOO_LOW"
)
self._inventory.append(item)
returnTrue
defremove_item(self, item_name):
"""移除物品"""
for i, item inenumerate(self._inventory):
if item.name == item_name:
returnself._inventory.pop(i)
raise ValueError(f"背包中没有物品: {item_name}")
classItem:
"""物品类"""
def__init__(self, name, required_level=1):
self.name = name
self.required_level = required_level
defdemo_correct_exception():
"""演示正确的异常使用方式"""
player = Player("小甲鱼", level=5)
# 测试正常情况
sword = Item("新手剑", required_level=1)
armor = Item("皮甲", required_level=3)
legendary_sword = Item("神器", required_level=10)
print("=" * 50)
print("🎮 异常处理最佳实践")
print("=" * 50)
# 添加低等级物品 - 成功
try:
player.add_item(sword)
print(f"✅ 添加 {sword.name} 成功")
except Exception as e:
print(f"❌ 添加失败: {e}")
# 添加中等级物品 - 成功
try:
player.add_item(armor)
print(f"✅ 添加 {armor.name} 成功")
except Exception as e:
print(f"❌ 添加失败: {e}")
# 添加高等级物品 - 失败(等级不足)
try:
player.add_item(legendary_sword)
print(f"✅ 添加 {legendary_sword.name} 成功")
except GameLogicError as e:
print(f"❌ 游戏逻辑错误: {e}")
print(f" 错误代码: {e.error_code}")
except ValueError as e:
print(f"❌ 值错误: {e}")
# 模拟满背包
for i inrange(19):
player.add_item(Item(f"物品{i}"))
try:
player.add_item(Item("第20个物品"))
except ValueError as e:
print(f"\n❌ {e}")
if __name__ == "__main__":
demo_correct_exception()
避坑要点:自定义异常是工具,不是装饰品
只有当内置异常无法准确描述你的业务错误时,才考虑自定义异常
而且层级不要超过两层(基类 + 具体类)
总结:5个避坑要点
好了,5个坑都讲完了
让我来总结一下最核心的避坑要点:
1. 永远不要用裸奔的except
# ❌ 错误
except:
print("出错了")
# ✅ 正确
except ValueError:
print("输入错误")
2. 先捕获具体异常,再捕获通用异常
# ❌ 错误
except Exception:
print("错误")
# ✅ 正确
except ValueError:
print("输入错误")
except Exception:
print("其他错误")
3. 永远指定异常对象
# ❌ 错误
except ValueError:
print("错误")
# ✅ 正确
except ValueError as e:
print(f"错误: {e}")
4. finally只做清理工作
# ❌ 错误
try:
save_game()
finally:
# 不要在这里保存!
pass
# ✅ 正确
try:
withopen("save.txt", "w") as f:
save_game(f)
except IOError:
print("保存失败")
5. 内置异常优先,自定义异常克制
# ❌ 过度设计
classMyError(Exception): pass
classMyError1(MyError): pass
classMyError2(MyError): pass
# ✅ 简约设计
if x < 0:
raise ValueError("x必须为正")
行动建议
今天回去做两件事:
- 1. 打开你最近的Python项目,搜索
except:和except Exception:,把它们都改成具体的异常类型 - 2. 写一个扫雷游戏,把今天学的异常处理都用上去。边界检查、文件操作、用户输入——这些都是异常处理的好场景
代码不报错的时候,是学异常处理的最佳时机
等到真正出错再学,就晚了
有问题,欢迎在评论区留言
下期见