🐍 Day 6: Python 异常处理 — 防御式编程的核心

学习路径: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 :会拦截 KeyboardInterruptSystemExit,让你无法 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 上下文管理器中的异常处理,就能写出健壮的代码。

相关推荐
朝发如雪1 小时前
快速掌握Linux(2)(时间相关函数)(文件IO)
linux·python·算法
叫我少年1 小时前
Git SSH 配置:从生成密钥到远程连接
git·后端
IT_陈寒1 小时前
Python多进程池的坑:子进程竟然不会退出
前端·人工智能·后端
苏子寒1 小时前
Nano-VLLM全代码解析笔记(2)-block_manager
人工智能·笔记·python·机器学习·nlp·vllm
小猴子爱上树1 小时前
跨境电商图片翻译工具,跨马批量处理视频字幕与抠图
python·音视频
smj2302_796826521 小时前
解决leetcode第4033题有效K个不同元素子数组I
数据结构·python·算法·leetcode
Patrick在香港1 小时前
Python asyncio vs 多线程 vs 多进程——同一份 HKOpenDataClient 实测 5/30/100 endpoint,谁是真的银弹
开发语言·python
Metaphor6921 小时前
使用 Python 设置 Excel 文件属性
开发语言·python·excel
IT毕设实战小研2 小时前
基于大数据的桂林热门旅游数据分析与可视化
大数据·python·机器学习·数据分析·django·课程设计·旅游