Python 基础教程:异常处理与调试技巧

前言

异常处理是区分”能用”与”用好”Python 的分水岭。优秀的异常处理能让程序在出错时优雅降级、留下可追溯的日志;而糟糕的异常处理则会让 bug 藏匿于无声的失败中,排查成本成倍增加。

本文将系统讲解:

  • 异常机制的底层原理与语法细节
  • 自定义异常的设计模式
  • 调试工具的实战用法(pdb、logging、traceback)
  • 反模式警示与最佳实践
  • 常见场景的代码模板

异常基础:try/except/finally

基本语法结构

# 基础结构
try:
    # 可能抛出异常的代码
    result = 10 / 0
except ZeroDivisionError:
    # 处理特定异常
    print("除数不能为零")
except (ValueError, TypeError) as e:
    # 同时捕获多种异常
    print(f"值错误: {e}")
except Exception as e:
    # 兜底捕获所有异常
    print(f"未知错误: {e}")
else:
    # 仅在 try 块成功时执行(可选)
    print("计算成功")
finally:
    # 无论是否异常都执行(可选)
    print("清理资源")

异常捕获的执行顺序

# 异常捕获顺序很重要!
​
try:
    raise ValueError("原始错误")
except ValueError as e:
    print(f"捕获 ValueError: {e}")  # 先匹配这个
except Exception as e:
    print(f"捕获 Exception: {e}")  # 永远不会执行

原则:子异常类必须写在父异常类前面

# ❌ 错误示范
try:
    risky_operation()
except Exception:      # 永远先匹配这个
    pass
except ValueError:      # 永远不会执行
    pass
​
# ✅ 正确写法
try:
    risky_operation()
except ValueError:      # 先匹配具体的
    pass
except Exception:        # 再匹配通用的
    pass

异常对象的核心属性

try:
    1 / 0
except ZeroDivisionError as e:
    print(f"异常类型: {type(e).__name__}")      # ZeroDivisionError
    print(f"错误信息: {e}")                      # division by zero
    print(f"完整追踪:\n{traceback.format_exc()}")  # 堆栈追踪
​
    # 异常对象的常用属性
    print(f"异常发生位置: {e.__traceback__.tb_frame.f_code.co_filename}")
    print(f"异常函数名: {e.__traceback__.tb_frame.f_code.co_name}")
    print(f"行号: {e.__traceback__.tb_lineno}")

重新抛出异常

# 方法1:直接 raise(保持原始异常)
try:
    some_operation()
except SomeError:
    print("记录日志")
    raise  # 重新抛出,不带参数
​
# 方法2:raise from(显式异常链)
try:
    db.connect()
except ConnectionError as e:
    raise ValueError("无法连接到数据库") from e  # 新异常源于原异常
​
# 方法3:raise from None(隐藏异常链)
try:
    legacy_code()
except Exception:
    raise NewError("新错误") from None  # 隐藏原始异常

异常层级与自定义异常

Python 内置异常层级

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── StopIteration
    ├── ArithmeticError
    │   ├── FloatingPointError
    │   ├── OverflowError
    │   └── ZeroDivisionError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError (IOError)
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── ValueError
    ├── TypeError
    └── ...

自定义异常设计原则

# 原则1:继承合适的基类
class AppError(Exception):
    """应用层异常基类"""
    pass
​
class ValidationError(AppError):
    """数据验证错误"""
    pass
​
class AuthenticationError(AppError):
    """认证错误"""
    pass
​
class NetworkError(AppError):
    """网络相关错误"""
    pass
​
# 原则2:添加有意义的属性
class APIError(AppError):
    """API 调用错误"""
    
    def __init__(self, code, message, response=None):
        self.code = code
        self.message = message
        self.response = response
        super().__init__(f"[{code}] {message}")
​
# 使用
raise APIError(404, "资源不存在", response={"id": 123})

异常工厂模式

class DatabaseError(Exception):
    """数据库错误工厂"""
    
    @staticmethod
    def connection_failed(host, reason):
        return DatabaseError(f"连接数据库 {host} 失败: {reason}")
    
    @staticmethod
    def query_failed(sql, reason):
        return DatabaseError(f"执行 SQL 失败: {sql[:50]}... 原因: {reason}")
    
    @staticmethod
    def timeout(seconds):
        return DatabaseError(f"查询超时 ({seconds}s)")
​
# 使用
try:
    db.query(sql)
except Exception as e:
    raise DatabaseError.query_failed(sql, str(e))

上下文管理器与资源清理

with 语句原理

# 上下文管理器协议
class MyResource:
    def __enter__(self):
        """进入 with 块时调用"""
        print("获取资源")
        return self  # as 后的变量接收此值
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """离开 with 块时调用"""
        if exc_type:
            print(f"异常类型: {exc_type.__name__}")
            print(f"异常值: {exc_val}")
            # 返回 True 抑制异常,False 或 None 传播异常
            return False
        print("释放资源")
        return False
​
# 使用
with MyResource() as r:
    print("使用资源中...")
    # 如果这里抛异常,__exit__ 会收到异常信息

@contextmanager 装饰器

from contextlib import contextmanager
​
@contextmanager
def managed_resource(name):
    """简化版上下文管理器"""
    print(f"获取 {name}")
    try:
        yield name  # 相当于 __enter__ 返回值
    finally:
        print(f"释放 {name}")
​
with managed_resource("数据库连接") as conn:
    print(f"使用 {conn}")
​
# 输出:
# 获取 数据库连接
# 使用 数据库连接
# 释放 数据库连接

嵌套上下文与组合

from contextlib import ExitStack
​
# 场景:需要同时管理多个资源
def process_files(files):
    """同时打开多个文件"""
    with ExitStack() as stack:
        file_handles = [
            stack.enter_context(open(f, 'r'))
            for f in files
        ]
        # 处理文件
        for f in file_handles:
            process(f)
    # 所有文件自动关闭
​
# 场景:条件性资源清理
@contextmanager
def temporary_file(path):
    """临时文件,使用后自动删除"""
    try:
        yield Path(path)
    finally:
        if Path(path).exists():
            Path(path).unlink()

异常与 finally 的陷阱

# 陷阱1:finally 中 return 会覆盖 try 中的异常
def bad_example():
    try:
        raise ValueError("原始异常")
    finally:
        return 42  # 异常被吞掉,函数返回 42
​
print(bad_example())  # 输出 42,不见异常
​
# 陷阱2:finally 先于 except 中的 return 执行
def flow_demo():
    try:
        print("1 - try 开始")
        raise ValueError()
    except:
        print("2 - except 捕获")
        return  # return 延迟到 finally 后执行
    finally:
        print("3 - finally 执行")
​
# 输出顺序: 1 -> 2 -> 3
​
# 陷阱3:finally 中的异常会覆盖原异常
def掩盖异常():
    try:
        raise ValueError("原异常")
    finally:
        raise RuntimeError("finally 中的异常")  # 原异常被掩盖
​
try:
    掩盖异常()
except RuntimeError as e:
    print(e)  # 只看到 "finally 中的异常"

调试工具全景图

1. 内置 traceback 模块

import traceback
import sys
​
# 格式化异常信息
def log_exception():
    exc_type, exc_value, exc_tb = sys.exc_info()
    
    print("=== 异常摘要 ===")
    print(f"类型: {exc_type.__name__}")
    print(f"信息: {exc_value}")
    
    print("\n=== 完整堆栈 ===")
    traceback.print_tb(exc_tb)
    
    # 获取字符串格式
    stack_str = "".join(traceback.format_tb(exc_tb))
    print(f"\n堆栈字符串:\n{stack_str}")
​
# 使用
try:
    [1, 2, 3][10]  # 触发 IndexError
except:
    log_exception()

2. 高级 traceback 格式化

import traceback
import sys
from pathlib import Path
​
def print_exception_details(exc_type, exc_value, exc_tb, max_lines=20):
    """美化异常输出"""
    if not exc_tb:
        return
    
    print("═" * 60)
    print(f"🔴 {exc_type.__name__}: {exc_value}")
    print("═" * 60)
    
    # 逐帧显示
    tb = exc_tb
    frames = []
    while tb:
        frame = tb.tb_frame
        code = frame.f_code
        frames.append({
            "file": Path(code.co_filename).name,
            "func": code.co_name,
            "line": tb.tb_lineno,
            "locals": {k: repr(v)[:50] 
                      for k, v in frame.f_locals.items() 
                      if not k.startswith('_')}
        })
        tb = tb.tb_next
    
    # 过滤到最近几帧
    for i, frame in enumerate(reversed(frames[-max_lines:])):
        print(f"\n📍 Frame {len(frames) - i - 1}")
        print(f"   文件: {frame['file']}")
        print(f"   函数: {frame['func']}()")
        print(f"   行号: {frame['line']}")
        if frame['locals']:
            print(f"   局部变量:")
            for k, v in list(frame['locals'].items())[:5]:
                print(f"     {k} = {v}")
    
    print("\n" + "═" * 60)
​
# 使用
try:
    def inner():
        x = 42
        y = [1, 2]
        return y[x]
    
    inner()
except:
    print_exception_details(*sys.exc_info())

3. pdb 调试器实战

# 方法1:代码中设置断点
import pdb
​
def buggy_function(data):
    pdb.set_trace()  # 程序在此暂停,进入交互式调试器
    result = []
    for i, item in enumerate(data):
        # 在调试器中可以:
        # - n (next): 执行下一行
        # - s (step): 进入函数
        # - p variable: 打印变量
        # - l (list): 查看当前代码
        # - c (continue): 继续执行
        # - q (quit): 退出调试
        result.append(item * 2)
    return result
​
buggy_function([1, 2, 3])
​
# 方法2:命令行启动
# python -m pdb script.py
​
# 方法3:事后调试
import pdb
import traceback
​
try:
    risky_code()
except Exception:
    pdb.pm()  # 事后进入调试器,查看异常发生时的状态

4. breakpoint() 内置函数(Python 3.7+)

# Python 3.7+ 的 breakpoint() 更强大
breakpoint()  # 等价于 pdb.set_trace()
​
# 配置使用其他调试器
import debugpy
breakpoint()  # 自动使用 debugpy
​
# 或在环境中设置
# export PYTHONBREAKPOINT=ipdb.set_trace

5. logging 模块高级用法

import logging
from pathlib import Path
​
# 配置日志系统
def setup_logger(name, log_file=None, level=logging.DEBUG):
    logger = logging.getLogger(name)
    logger.setLevel(level)
    
    formatter = logging.Formatter(
        '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    # 控制台输出
    console = logging.StreamHandler()
    console.setFormatter(formatter)
    logger.addHandler(console)
    
    # 文件输出(带轮转)
    if log_file:
        file_handler = logging.FileHandler(log_file, encoding='utf-8')
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)
    
    return logger
​
# 使用
logger = setup_logger(__name__, 'app.log')
​
try:
    result = calculate()
    logger.info(f"计算成功: {result}")
except ValueError as e:
    logger.error(f"验证错误: {e}", exc_info=True)  # exc_info=True 打印堆栈
except Exception as e:
    logger.critical(f"系统错误: {e}", exc_info=True)
    raise
​
# 条件日志(避免昂贵操作的日志开销)
if logger.isEnabledFor(logging.DEBUG):
    logger.debug(f"详细数据: {expensive_to_serialize()}")

实战调试技巧

技巧1:断言的艺术

# 断言用于开发期捕获"不可能发生"的错误
# 生产环境可用 -O 标志禁用
​
def calculate_discount(price, discount):
    assert 0 <= discount <= 1, f"折扣率必须在 0-1 之间,当前值: {discount}"
    assert price >= 0, f"价格不能为负: {price}"
    
    final_price = price * (1 - discount)
    assert final_price >= 0, "计算结果异常"
    
    return final_price
​
# 使用自定义 AssertionError
class PriceError(AssertionError):
    """价格计算错误"""
    pass
​
def safe_divide(a, b):
    if b == 0:
        raise PriceError(f"除数不能为零: a={a}, b={b}")
    return a / b

技巧2:异常链与日志追踪

import logging
import traceback
​
logger = logging.getLogger(__name__)
​
def process_data(raw_data):
    """异常处理与日志记录的标准模式"""
    try:
        # 解析数据
        data = parse(raw_data)
        
        # 验证数据
        validate(data)
        
        # 处理数据
        return transform(data)
        
    except ValidationError as e:
        logger.warning(f"数据验证失败: {e}")
        raise DataError(f"无效数据: {e}") from e
        
    except Exception as e:
        logger.error(
            f"处理数据时发生未知错误: {e}",
            exc_info=True,  # 打印完整堆栈
            extra={"raw_data": str(raw_data)[:100]}  # 添加上下文
        )
        raise ProcessingError("数据处理失败") from e

技巧3:使用警告系统

import warnings
​
# 警告不会终止程序,但能引起注意
warnings.warn("这个 API 已弃用,请使用新方法", DeprecationWarning)
warnings.warn("性能可能受影响", PerformanceWarning)
​
# 警告过滤器配置
warnings.filterwarnings(
    "error",  # 将警告转为异常
    category=DeprecationWarning
)
​
# 按模块过滤
warnings.filterwarnings(
    "ignore",
    message=".*deprecated.*",
    category=PendingDeprecationWarning
)

技巧4:数据校验模式

from typing import Any, Callable
​
def validate(value: Any, *rules: Callable) -> Any:
    """链式数据校验"""
    for rule in rules:
        if not rule(value):
            raise ValidationError(f"值 {value!r} 不满足规则: {rule.__name__}")
    return value
​
def is_positive(x): return x > 0
def is_integer(x): return isinstance(x, int)
def in_range(min_v, max_v):
    def _check(x): return min_v <= x <= max_v
    return _check
​
# 使用
try:
    age = validate(
        user_input,
        is_integer,
        is_positive,
        in_range(0, 150)
    )
except ValidationError as e:
    print(f"校验失败: {e}")

技巧5:异常处理装饰器

import functools
import time
import logging
​
logger = logging.getLogger(__name__)
​
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """重试装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts - 1:
                        raise
                    logger.warning(
                        f"{func.__name__} 第 {attempt + 1} 次失败: {e},"
                        f"{delay}s 后重试..."
                    )
                    time.sleep(delay)
        return wrapper
    return decorator
​
def handle_errors(default=None, log=True):
    """异常处理装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                if log:
                    logger.error(f"{func.__name__} 执行失败: {e}")
                if callable(default):
                    return default(e)
                return default
        return wrapper
    return decorator
​
# 使用
@retry(max_attempts=3, delay=2, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
    return requests.get(url)
​
@handle_errors(default={}, log=True)
def parse_json(data):
    return json.loads(data)

技巧6:IPython/jupyter 调试增强

# 在 IPython 或 Jupyter 中使用
%debug  # 事后调试最近的异常
%timeit # 测试代码执行时间
​
# 变量检查
# %who    - 列出所有变量
# %whos   - 列出所有变量及类型
# %pdb    - 自动启用 pdb

常见反模式与最佳实践

反模式警示

反模式问题正确做法
except: pass吞掉所有异常except SomeError: handle_it()
except Exception过于宽泛具体异常类型
raise Exception(e)丢失原始堆栈raise 不带参数重新抛出
异常用于流程控制可读性差使用 if/else
异常中执行 IO性能问题异常应轻量

异常处理最佳实践清单

# ✅ 1. 具体异常优先
try:
    config = json.load(f)
except json.JSONDecodeError as e:
    logger.error(f"配置文件格式错误: {e}")
​
# ❌ 2. 避免过于宽泛的捕获
try:
    config = json.load(f)
except Exception:  # 捕获了文件不存在、权限问题等
    pass
​
# ✅ 3. 异常包含上下文
class OrderNotFoundError(Exception):
    def __init__(self, order_id):
        self.order_id = order_id
        super().__init__(f"订单不存在: {order_id}")
​
raise OrderNotFoundError(order_id)
​
# ❌ 4. 避免异常中的副作用
try:
    process(data)
except Exception:
    cleanup()  # 异常处理函数不应抛异常
    raise
​
# ✅ 5. EAFP vs LBYL 风格选择
# EAFP (Easier to Ask for Forgiveness than Permission)
try:
    value = data[key]
except KeyError:
    value = default
​
# LBYL (Look Before You Leap)
if key in data:
    value = data[key]
else:
    value = default
​
# 建议:字典/列表用 EAFP,文件/网络用 LBYL(性能更好)

分层异常处理

# 层级1:最外层 - 统一错误响应(API/Web)
@app.errorhandler(ValidationError)
def handle_validation(e):
    return {"error": str(e)}, 400
​
# 层级2:业务逻辑层 - 异常转换
class OrderService:
    def create_order(self, data):
        try:
            self.validate(data)
            return self.repository.save(data)
        except DBError as e:
            raise OrderError(f"创建订单失败: {e}") from e
​
# 层级3:数据访问层 - 具体异常
class OrderRepository:
    def save(self, data):
        try:
            return self.db.insert(data)
        except IntegrityError:
            raise DBError("数据完整性冲突")

排错指南

常见错误与解决方案

错误1:异常被意外吞掉

# 问题
try:
    risky_operation()
except:
    pass  # 所有异常都消失得无影无踪
​
# 诊断:添加日志
import logging
logger = logging.getLogger(__name__)
​
try:
    risky_operation()
except Exception as e:
    logger.exception("操作失败")  # 自动包含堆栈
    raise

错误2:异常信息不完整

# 问题:只有错误类型,没有上下文
try:
    process(user_id=123)
except UserNotFoundError:
    raise ValueError("用户不存在")  # 丢失 user_id 信息
​
# 解决:包含所有上下文
try:
    process(user_id=123)
except UserNotFoundError:
    raise ValueError(f"用户不存在: user_id={123}") from None

错误3:finally 中的异常掩盖原异常

# 问题
try:
    raise OriginalError("原异常")
finally:
    cleanup()  # 如果 cleanup 抛异常,原异常丢失
​
# 解决:显式处理
try:
    raise OriginalError("原异常")
except OriginalError:
    try:
        cleanup()
    except:
        pass  # 记录但不传播 cleanup 的异常
    raise  # 重新抛出原异常

错误4:异步代码中的异常

import asyncio
​
async def bad_async():
    try:
        await risky_async_call()
    except Exception:
        # 异常在异步任务中传播
        pass  # 异常被吞掉!
​
# 正确:确保异常被记录
async def good_async():
    try:
        await risky_async_call()
    except Exception as e:
        logger.exception(f"异步任务失败: {e}")
        raise

错误5:循环中的异常处理

# 问题
for item in items:
    try:
        process(item)
    except Exception:
        pass  # 一个失败就静默跳过
​
# 解决:记录所有错误
errors = []
for item in items:
    try:
        process(item)
    except Exception as e:
        errors.append((item, e))
​
if errors:
    logger.error(f"处理失败 {len(errors)} 项:")
    for item, e in errors:
        logger.error(f"  {item}: {e}")

调试工作流

# 1. 复现问题
# 确保能稳定触发问题
​
# 2. 隔离问题
def isolate_issue():
    # 二分法逐步缩小范围
    pass
​
# 3. 添加诊断日志
import logging
logging.basicConfig(level=logging.DEBUG)
​
# 4. 使用断点
import pdb; pdb.set_trace()
​
# 5. 编写最小复现用例
def test_reproduction():
    """最小化复现代码"""
    raise AssertionError("期望与实际不符")
​
# 6. 修复并验证
# 7. 添加回归测试
import pytest
​
def test_that_fails_then_passes():
    """捕获之前失败的场景"""
    with pytest.raises(CustomError):
        risky_function()

总结

核心要点

  1. 异常捕获要具体:优先捕获最具体的异常类型
  2. 保留异常链:使用 raise ... from e 保持上下文
  3. 上下文管理器with 语句确保资源正确释放
  4. 日志记录logger.exception() 自动包含堆栈信息
  5. 防御性编程:结合断言与异常处理

一图总结

┌─────────────────────────────────────────────────────────────┐
│                    异常处理速查表                             │
├──────────────────┬──────────────────────────────────────────┤
│     场景         │                   做法                    │
├──────────────────┼──────────────────────────────────────────┤
│ 捕获多种异常      │ except (A, B) as e:                      │
│ 重新抛出异常      │ raise  # 不带参数                        │
│ 异常链           │ raise NewError() from e                  │
│ 抑制异常链        │ raise NewError() from None               │
│ 记录并重新抛出    │ logger.exception(); raise                │
│ 资源清理         │ with resource: ... # 自动清理            │
│ 重试机制         │ @retry(max_attempts=3)                   │
│ 事后调试         │ pdb.pm() / %debug (IPython)              │
└──────────────────┴──────────────────────────────────────────┘

推荐工具链

场景工具说明
交互式调试pdb / ipdb命令行调试器
图形调试IDE 内置VS Code / PyCharm
日志logging标准库,推荐配置后使用
性能分析cProfile内置性能分析器
单元测试pytest完善的断言与异常测试支持
静态检查mypy / pyright类型检查可发现潜在错误

💡 提示: 生产环境中,建议使用 Sentry、Bugsnag 等 APM 工具收集异常信息。 📚 扩展阅读:


原创内容,版权所有。未经授权,禁止转载。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容