学习路径:Python_AIGC · Day 6
异常不是错误,而是控制流。学会优雅地处理异常,代码才能跑在生产环境。
一、异常处理五件套
python
try:
# 1. 执行可能出错的代码
num = int(input("输入一个数字: "))
result = 10 / num
except ValueError:
# 2. 捕获特定异常
print("❌ 输入不是有效数字")
except ZeroDivisionError:
print("❌ 不能除以零")
except Exception as e:
# 3. 兜底捕获(尽量不要裸 except:)
print(f"❌ 未知错误: {e}")
else:
# 4. 没有异常时执行
print(f"✅ 结果是: {result}")
finally:
# 5. 不管有无异常都执行
print("🏁 清理工作")
各子句的执行顺序
text
try 块
├── 无异常 → else 块 → finally 块
├── 有异常且被 except 捕获 → except 块 → finally 块
└── 有异常但未被捕获 → finally 块 → 异常向上传播
捕获多个异常
python
# 方式一:元组捕获
try:
data = {"key": "value"}
print(data["missing"])
result = 1 / 0
except (KeyError, ZeroDivisionError) as e:
print(f"捕获到: {type(e).__name__}: {e}")
# 方式二:链式 except(从上到下匹配,只进一个)
try:
value = [1, 2, 3][10]
except IndexError:
print("索引越界")
except LookupError: # LookupError 是 IndexError 的父类
print("查找错误")
# 输出:索引越界(先匹配到子类)
# 方式三:捕获并重新引发
try:
risky_operation()
except ValueError:
print("记录日志...")
raise # 保留原始 traceback,不传参
二、raise 与自定义异常
手动抛出异常
python
def withdraw(balance: float, amount: float) -> float:
if amount <= 0:
raise ValueError("取款金额必须为正数")
if amount > balance:
raise ValueError("余额不足")
return balance - amount
# 异常链(Python 3 自动链式)
try:
withdraw(100, 50)
except ValueError as e:
# 保留上下文,不丢失原始异常
raise RuntimeError("交易处理失败") from e
自定义异常
python
class PaymentError(Exception):
"""支付相关异常的基类"""
pass
class InsufficientBalanceError(PaymentError):
def __init__(self, balance: float, amount: float):
self.balance = balance
self.amount = amount
super().__init__(f"余额不足: 当前 ¥{balance:.2f},需要 ¥{amount:.2f}")
class CardExpiredError(PaymentError):
pass
# 使用
def pay(balance: float, amount: float) -> bool:
if balance < amount:
raise InsufficientBalanceError(balance, amount)
print(f"支付成功 ¥{amount:.2f}")
return True
try:
pay(100, 500)
except InsufficientBalanceError as e:
print(f"❌ {e}") # ❌ 余额不足: 当前 ¥100.00,需要 ¥500.00
print(f"差额: {e.amount - e.balance}") # 差额: 400.0
except PaymentError:
print("❌ 其他支付错误")
🔑 最佳实践 :自定义异常应继承自合理的基类,层级不要太深。项目可定义
AppError(Exception),所有业务异常继承它。
三、断言(assert)
assert 用于调试阶段 的条件检查,不要用于生产逻辑验证(python -O 会跳过)。
python
def divide(a: int, b: int) -> float:
assert b != 0, "除数不能为零"
return a / b
# 生产环境:python -O script.py 时 assert 被跳过
divide(10, 0) # 调试时:AssertionError
⚠️ 永远不要用 assert 做输入校验 ,应该用
if ... raise ValueError()。
四、Python 异常层级
text
BaseException
├── SystemExit # sys.exit()
├── KeyboardInterrupt # Ctrl+C
└── Exception # ✅ 绝大多数我们处理的是这个
├── ArithmeticError (ZeroDivisionError, OverflowError...)
├── LookupError (IndexError, KeyError...)
├── ValueError (类型对值不对)
├── TypeError (类型不对)
├── RuntimeError (运行时错误)
├── OSError (FileNotFoundError, PermissionError...)
├── json.JSONDecodeError
└── ...更多自定义
🔑 不要捕获
BaseException:会拦截KeyboardInterrupt和SystemExit,让你无法 Ctrl+C 退出程序。
五、with 语句与上下文管理
with 是我们已经学过但值得在异常场景下深入理解的语法:
python
# with 语句等价于 try/finally
file = None
try:
file = open("data.txt", "r")
data = file.read()
except FileNotFoundError:
print("文件不存在")
finally:
if file:
file.close()
# 但 with 更简洁
try:
with open("data.txt", "r") as f:
data = f.read()
except FileNotFoundError:
print("文件不存在")
# with 块结束时自动关闭,无需 finally
上下文管理器的异常传播
python
class ManagedResource:
def __enter__(self):
print("🟢 获取资源")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("🔴 释放资源")
if exc_type is ValueError:
print(f" 处理异常: {exc_val}")
return True # 吞噬异常,不向外传播
return False # 让异常继续传播
# 测试 1:正常执行
with ManagedResource() as res:
print(" 工作中...")
# 🟢 获取资源
# 工作中...
# 🔴 释放资源
# 测试 2:异常被 __exit__ 吞噬
with ManagedResource() as res:
raise ValueError("某种错误")
# 🟢 获取资源
# 🔴 释放资源
# 处理异常: 某种错误
# ← 程序继续,没有抛出异常
# 测试 3:异常未被吞噬
with ManagedResource() as res:
raise TypeError("其他错误")
# 🟢 获取资源
# 🔴 释放资源
# ← TypeError 继续向上传播
六、实战:健壮的配置文件读取器
python
import json
from pathlib import Path
from typing import Any, Optional
class ConfigError(Exception):
"""配置读取异常基类"""
pass
class ConfigNotFoundError(ConfigError):
"""配置文件不存在"""
pass
class ConfigParseError(ConfigError):
"""配置文件格式错误"""
pass
def load_config(path: str, default: Optional[dict] = None) -> dict[str, Any]:
"""
健壮的配置文件加载器
尝试加载 JSON 配置文件,如果失败则返回默认配置。
对文件不存在、JSON 格式错误、权限问题分别处理。
"""
config_path = Path(path)
try:
with open(config_path, "r", encoding="utf-8") as f:
content = f.read()
if not content.strip():
raise ConfigParseError("配置文件为空")
try:
config = json.loads(content)
except json.JSONDecodeError as e:
raise ConfigParseError(f"JSON 解析失败: {e}") from e
if not isinstance(config, dict):
raise ConfigParseError("配置必须是 JSON 对象(dict)")
return config
except FileNotFoundError:
raise ConfigNotFoundError(f"配置不存在: {path}")
except PermissionError:
raise ConfigError(f"无权限读取: {path}")
except ConfigError:
raise # 重新抛出上面的自定义异常
except Exception as e:
raise ConfigError(f"未知错误: {e}") from e
# ---- 测试 ----
def test_load_config():
config_path = "/tmp/test_config.json"
# 测试 1:文件不存在
try:
load_config("/tmp/nonexistent.json")
except ConfigNotFoundError as e:
print(f"✅ 文件不存在捕获: {e}")
# 测试 2:创建非法 JSON
with open(config_path, "w") as f:
f.write("not json")
try:
load_config(config_path)
except ConfigParseError as e:
print(f"✅ JSON 错误捕获: {e}")
# 测试 3:正常 JSON
with open(config_path, "w") as f:
json.dump({"debug": True, "port": 8080}, f)
config = load_config(config_path)
print(f"✅ 正确读取: {config}")
test_load_config()
📝 避坑指南
| 陷阱 | 说明 | 正确做法 |
|---|---|---|
裸 except: |
连 KeyboardInterrupt 也捕获 |
用 except Exception: |
| 吞异常静默 | 空 except: pass 不记录 |
至少 logging.exception() |
| 过于宽泛的捕获 | except Exception: 掩盖 bug |
尽量指定具体异常类型 |
| finally 里 return | 会覆盖 try 里的 return/异常 | finally 中不要 return |
| 异常消耗性能 | try 块不耗时,但抛出异常很慢 | 不要用异常做正常控制流 |
python
# ❌ 不要用异常做流程控制
def find_first(nums, target):
try:
while True:
i = nums.index(target)
return i
except ValueError:
return -1
# ✅ 用 if 替代
def find_first(nums, target):
try:
return nums.index(target)
except ValueError:
return -1
# ❌ finally 中 return 覆盖异常
def dangerous():
try:
raise ValueError("原始错误")
finally:
return "覆盖返回值"
print(dangerous()) # "覆盖返回值" --- 异常消失了!
Day 6 总结:异常处理是防御式编程的基础。掌握 try/except/else/finally 的完整语义、自定义异常体系、异常链、以及 with 上下文管理器中的异常处理,就能写出健壮的代码。