Administrator
发布于 2026-03-19 / 1 阅读
0
0

第九章:异常处理

第九章:异常处理

本章学习目标

  • 理解 Python 异常机制
  • 掌握 try-except 语法和高级用法
  • 学会自定义异常
  • 理解异常链和 traceback
  • 掌握上下文管理器与异常处理
  • 学会使用 logging 模块记录错误

9.1 异常基础

9.1.1 什么是异常

异常是程序执行过程中发生的错误,会中断正常的程序流程。与语法错误不同,异常发生在程序运行期间。

# 这些都会引发异常
result = 10 / 0           # ZeroDivisionError: division by zero
print(undefined_var)     # NameError: name 'undefined_var' is not defined
"123" + 456              # TypeError: can only concatenate str (not "int") to str
open("not_exist.txt")    # FileNotFoundError: [Errno 2] No such file or directory
lst = [1, 2, 3]
print(lst[10])           # IndexError: list index out of range
d = {"a": 1}
print(d["b"])            # KeyError: 'b'

9.1.2 异常层级结构

Python 的异常有一个层级结构:

BaseException
 ├── SystemExit              # sys.exit() 调用
 ├── KeyboardInterrupt       # Ctrl+C 中断
 ├── GeneratorExit           # 生成器关闭
 └── Exception               # 所有内置异常的基类
      ├── StopIteration
      ├── ArithmeticError
      │    ├── FloatingPointError
      │    ├── OverflowError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── OSError (IOError)
      │    ├── FileNotFoundError
      │    ├── PermissionError
      │    └── ...
      ├── TypeError
      ├── ValueError
      ├── RuntimeError
      └── ...

9.1.3 异常 vs 错误

  • 异常:程序逻辑问题,可以被捕获和处理
  • 错误:通常是系统级别的问题,如内存溢出

9.2 try-except 语句

9.2.1 基本语法

# 最简单的形式
try:
    result = 10 / 0
except ZeroDivisionError:
    print("不能除以零!")

# 带有异常对象
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"发生错误: {e}")
    print(f"异常类型: {type(e).__name__}")

9.2.2 多个 except

# 分别处理不同异常
try:
    value = int("abc")       # ValueError
    lst = [1, 2, 3]
    print(lst[10])           # IndexError
except ValueError:
    print("值转换错误")
except IndexError:
    print("索引错误")

# except 顺序:先具体后一般
try:
    result = 10 / 0
except ZeroDivisionError:
    print("除零错误")
except ArithmeticError:
    print("算术错误")
except Exception:
    print("其他错误")

9.2.3 捕获所有异常

# 捕获所有 Exception
try:
    # 可能发生任何异常的代码
    pass
except Exception as e:
    print(f"发生错误: {e}")

# 捕获所有异常(包括非 Exception 子类)
try:
    pass
except BaseException as e:
    print(f"异常: {e}")

9.2.4 捕获多个异常(元组形式)

# 在一个 except 中处理多个异常
try:
    # 可能引发多种异常的代码
    pass
except (ValueError, TypeError, KeyError) as e:
    print(f"发生错误: {e}")

9.2.5 else 子句

else 子句在没有异常发生时执行:

try:
    result = 10 / 2
except ZeroDivisionError:
    print("发生除零错误")
else:
    print(f"计算成功,结果是 {result}")
    # 只有没有异常时才会执行

9.2.6 finally 子句

finally 子句无论是否有异常都会执行:

# 基本用法
try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("文件不存在")
finally:
    # 始终执行
    if 'file' in locals():
        file.close()

# 典型用途:资源清理
try:
    result = 10 / 2
except ZeroDivisionError:
    print("错误")
finally:
    print("程序结束")

9.2.7 完整结构

try:
    # 可能发生异常的代码
    result = 10 / 2
except ZeroDivisionError:
    # 处理除零错误
    print("不能除以零")
except ValueError:
    # 处理值错误
    print("值错误")
except Exception as e:
    # 处理其他所有异常
    print(f"未知错误: {e}")
else:
    # 没有异常时执行
    print(f"结果是 {result}")
finally:
    # 始终执行
    print("清理工作")

9.3 抛出异常

9.3.1 raise 语句

使用 raise 语句抛出异常:

# 基本形式
def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(e)  # 除数不能为零

9.3.2 重新抛出异常

在 except 块中重新抛出异常:

def handle_error() -> None:
    try:
        # 可能异常的代码
        pass
    except Exception as e:
        print(f"记录错误: {e}")
        raise  # 重新抛出原始异常

9.3.3 raise from

Python 3 支持异常链:

# 显式异常链
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("处理失败") from e

# 隐式异常链
try:
    int("abc")
except ValueError:
    # 在 except 块中抛出的新异常会自动链接
    raise RuntimeError("处理失败")

9.3.4 断言 raise

# 使用 assert 抛出 AssertionError
def divide(a: float, b: float) -> float:
    assert b != 0, "除数不能为零"
    return a / b

9.4 自定义异常

9.4.1 定义异常类

# 简单的自定义异常
class ValidationError(Exception):
    """验证错误异常"""
    pass

# 带有属性的异常
class NegativeValueError(ValueError):
    """负数值错误"""
    def __init__(self, value: float, message: str | None = None) -> None:
        self.value = value
        self.message = message or f"值不能为负数: {value}"
        super().__init__(self.message)

9.4.2 使用自定义异常

def set_age(age: int) -> int:
    if age < 0:
        raise NegativeValueError(age, "年龄不能为负数")
    if age > 150:
        raise ValidationError("年龄超出合理范围")
    return age

try:
    set_age(-5)
except NegativeValueError as e:
    print(f"错误: {e.message}")
    print(f"无效值: {e.value}")
except ValidationError as e:
    print(f"验证错误: {e}")

9.4.3 异常层次结构

class AppError(Exception):
    """应用基础异常"""
    pass

class DataError(AppError):
    """数据相关错误"""
    pass

class ValidationError(DataError):
    """验证错误"""
    pass

class StorageError(AppError):
    """存储相关错误"""
    pass

# 使用
try:
    raise ValidationError("数据无效")
except AppError as e:
    print(f"应用错误: {e}")  # 会被捕获
except ValidationError as e:
    print(f"验证错误: {e}")

9.4.4 实战:验证器示例

class ValidationError(Exception):
    """验证错误"""
    def __init__(self, field: str, message: str) -> None:
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")


class Validator:
    """数据验证器"""

    @staticmethod
    def validate_email(email: str) -> None:
        if "@" not in email:
            raise ValidationError("email", "邮箱格式无效")

    @staticmethod
    def validate_age(age: int) -> None:
        if age < 0:
            raise ValidationError("age", "年龄不能为负数")
        if age > 150:
            raise ValidationError("age", "年龄超出合理范围")

    @staticmethod
    def validate_user(data: dict) -> None:
        errors: list[str] = []

        try:
            Validator.validate_email(data.get("email", ""))
        except ValidationError as e:
            errors.append(str(e))

        try:
            Validator.validate_age(data.get("age", 0))
        except ValidationError as e:
            errors.append(str(e))

        if errors:
            raise ValidationError("user", "; ".join(errors))


# 使用
user_data = {"email": "invalid", "age": -5}
try:
    Validator.validate_user(user_data)
except ValidationError as e:
    print(f"验证失败: {e.field}")
    print(e.message)

9.5 traceback 模块

9.5.1 打印异常信息

import traceback

try:
    1 / 0
except Exception:
    # 打印完整的 traceback
    traceback.print_exc()

    # 获取为字符串
    error_str = traceback.format_exc()
    print(error_str)

9.5.2 记录 traceback

import logging
import traceback

logging.basicConfig(level=logging.ERROR)

try:
    1 / 0
except Exception:
    logging.error("发生错误:\n%s", traceback.format_exc())

9.5.3 手动控制 traceback

import traceback

try:
    # 可能异常的代码
    pass
except Exception:
    # 保存 traceback
    exc_type, exc_value, exc_tb = sys.exc_info()

    # 格式化
    tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
    print(tb_str)

9.6 上下文管理器与异常

9.6.1 contextmanager

from contextlib import contextmanager

@contextmanager
def timer(name: str):
    """计时上下文管理器"""
    import time
    start = time.time()
    try:
        yield  # 执行 with 块中的代码
    finally:
        end = time.time()
        print(f"{name} 耗时: {end - start:.2f}秒")

# 使用
with timer("计算"):
    result = sum(range(1000000))

9.6.2 suppress

from contextlib import suppress
import os

# 忽略特定异常
with suppress(FileNotFoundError):
    os.remove("not_exist.txt")

# 等价于
try:
    os.remove("not_exist.txt")
except FileNotFoundError:
    pass

9.6.3 ExitStack

from contextlib import ExitStack

# 管理多个上下文管理器
with ExitStack() as stack:
    files = [stack.enter_context(open(f"file{i}.txt", "r")) for i in range(3)]
    # 所有文件会在离开 with 块时自动关闭

9.6.4 redirect_stderr / redirect_stdout

from contextlib import redirect_stderr
import sys

# 重定向 stderr
with open("error.log", "w") as f:
    with redirect_stderr(f):
        print("这条消息会写入文件", file=sys.stderr)

9.7 logging 模块

9.7.1 基础配置

import logging

# 基础配置
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

# 获取 logger
logger = logging.getLogger(__name__)

# 使用
logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误")

9.7.2 日志级别

级别 数值 说明
CRITICAL 50 严重错误
ERROR 40 错误
WARNING 30 警告
INFO 20 信息
DEBUG 10 调试
NOTSET 0 不设置

9.7.3 记录异常

import logging

logger = logging.getLogger(__name__)

try:
    1 / 0
except Exception:
    # 使用 exc_info=True 记录异常信息
    logger.exception("发生错误")
    # 等价于
    # logger.error("发生错误", exc_info=True)

9.7.4 日志配置

import logging

# 创建 logger
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# 创建处理器
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler("app.log", encoding="utf-8")

console_handler.setLevel(logging.INFO)
file_handler.setLevel(logging.DEBUG)

# 创建格式化器
formatter = logging.Formatter(
    "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# 绑定
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)

# 添加处理器
logger.addHandler(console_handler)
logger.addHandler(file_handler)

# 使用
logger.info("信息")
logger.error("错误")

9.7.5 实战:完整的异常处理

import logging
import sys
from pathlib import Path


class ErrorHandler:
    """错误处理器"""

    def __init__(self, log_file: str = "error.log") -> None:
        self.log_file = Path(log_file)
        self._setup_logging()

    def _setup_logging(self) -> None:
        """配置日志"""
        self.logger = logging.getLogger("app")
        self.logger.setLevel(logging.DEBUG)

        # 文件处理器
        file_handler = logging.FileHandler(
            self.log_file, encoding="utf-8"
        )
        file_handler.setLevel(logging.DEBUG)

        # 控制台处理器
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)

        # 格式化
        formatter = logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        )
        file_handler.setFormatter(formatter)
        console_handler.setFormatter(formatter)

        self.logger.addHandler(file_handler)
        self.logger.addHandler(console_handler)

    def handle_exception(self, exc_type, exc_value, exc_tb) -> None:
        """处理未捕获的异常"""
        if issubclass(exc_type, KeyboardInterrupt):
            sys.__excepthook__(exc_type, exc_value, exc_tb)
            return

        self.logger.critical(
            "未捕获的异常",
            exc_info=(exc_type, exc_value, exc_tb)
        )

    def install(self) -> None:
        """安装全局异常处理器"""
        sys.excepthook = self.handle_exception


# 使用
def main():
    handler = ErrorHandler("app.log")

    # 模拟错误
    try:
        result = 10 / 0
    except Exception:
        handler.logger.exception("计算错误")

    handler.install()


if __name__ == "__main__":
    main()

9.8 异常最佳实践

9.8.1 原则

  1. 捕获具体异常:不要使用 except:
# ✗ 不推荐
try:
    do_something()
except:
    pass

# ✓ 推荐
try:
    do_something()
except ValueError as e:
    print(f"值错误: {e}")
except TypeError as e:
    print(f"类型错误: {e}")
  1. 保留异常信息:使用 from e 保留原始异常
# ✗ 不推荐
try:
    int("abc")
except ValueError:
    raise RuntimeError("处理失败")

# ✓ 推荐
try:
    int("abc")
except ValueError as e:
    raise RuntimeError("处理失败") from e
  1. 不要忽略异常:至少记录日志
# ✗ 不推荐
try:
    do_something()
except Exception:
    pass  # 静默忽略

# ✓ 推荐
try:
    do_something()
except Exception as e:
    logging.warning(f"跳过错误: {e}")
  1. 使用 finally 清理资源
# 文件操作
file = None
try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("文件不存在")
finally:
    if file:
        file.close()

# 推荐使用 with 语句

9.8.2 异常处理示例

import logging

def read_number(filename: str) -> int | None:
    """读取文件中的数字"""
    logger = logging.getLogger(__name__)

    try:
        with open(filename, "r", encoding="utf-8") as f:
            content = f.read().strip()
            return int(content)
    except FileNotFoundError:
        logger.error(f"文件不存在: {filename}")
    except PermissionError:
        logger.error(f"没有权限读取: {filename}")
    except ValueError:
        logger.error(f"文件内容不是有效数字")
    except Exception as e:
        logger.exception(f"未知错误: {e}")

    return None

综合示例

示例 1:安全的除法函数

def safe_divide(a: float, b: float) -> float | None:
    """安全的除法运算"""
    try:
        if b == 0:
            raise ZeroDivisionError("除数不能为零")
        return a / b
    except ZeroDivisionError as e:
        print(f"错误: {e}")
        return None


def divide(a: float, b: float) -> float:
    """除法运算,失败时抛出异常"""
    if b == 0:
        raise ZeroDivisionError(f"不能除以零: {a} / {b}")
    return a / b


# 测试
print(safe_divide(10, 2))   # 5.0
print(safe_divide(10, 0))    # None
print(divide(10, 2))         # 5.0

示例 2:配置加载器

import json
from pathlib import Path


class ConfigLoadError(Exception):
    """配置加载错误"""
    pass


def load_config(filepath: str) -> dict:
    """加载配置文件"""
    path = Path(filepath)

    if not path.exists():
        raise ConfigLoadError(f"配置文件不存在: {filepath}")

    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError as e:
        raise ConfigLoadError(f"配置文件格式错误: {e}")
    except PermissionError:
        raise ConfigLoadError(f"没有权限读取配置文件")


# 测试
try:
    config = load_config("config.json")
except ConfigLoadError as e:
    print(f"加载配置失败: {e}")

示例 3:重试机制

import time
from functools import wraps


def retry(max_attempts: int = 3, delay: float = 1.0):
    """重试装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts:
                        print(f"第 {attempt} 次尝试失败,{delay}秒后重试...")
                        time.sleep(delay)
                    else:
                        print(f"重试 {max_attempts} 次后仍然失败")
            raise last_exception
        return wrapper
    return decorator


@retry(max_attempts=3, delay=1.0)
def unstable_function():
    """模拟不稳定的函数"""
    import random
    if random.random() > 0.7:
        return "成功!"
    raise RuntimeError("随机失败")


# 测试
for _ in range(5):
    try:
        result = unstable_function()
        print(f"结果: {result}")
    except RuntimeError as e:
        print(f"最终失败: {e}")

课后练习

练习 9.1:基本异常处理

实现一个安全的除法函数,处理除数为零的情况。

练习 9.2:自定义异常

创建一个 InvalidEmailError 异常类,用于验证邮箱格式。

练习 9.3:文件读取

实现一个安全的文件读取函数,处理文件不存在、权限错误等情况。

练习 9.4:上下文管理器

实现一个计时上下文管理器,测量代码执行时间。

练习 9.5:验证器

创建一个完整的输入验证函数,支持:

  1. 验证邮箱格式
  2. 验证手机号
  3. 验证密码强度
  4. 收集所有错误

练习 9.6:重试装饰器

实现一个重试装饰器,支持最大重试次数和延迟。

练习 9.7:日志系统

创建一个完整的日志系统,支持:

  1. 控制台输出
  2. 文件输出
  3. 日志轮转
  4. 异常记录

本章小结

本章我们详细学习了 Python 的异常处理:

  1. 异常基础

    • 异常的概念和层级结构
    • 常见异常类型
  2. try-except

    • 基本语法
    • 多个 except
    • else 和 finally
    • 捕获所有异常
  3. 抛出异常

    • raise 语句
    • 异常链
  4. 自定义异常

    • 定义异常类
    • 异常层次结构
  5. traceback

    • 打印和记录异常信息
  6. 上下文管理器

    • 与异常处理结合
  7. logging

    • 日志配置和使用

掌握异常处理是编写健壮程序的关键技能。

相关资源


评论