Python 错误处理与单元测试

Python(04)学习笔记

发布于 2025-04-13

1. Python 异常处理基础

1.1 基本异常处理结构

try:
    # 可能引发异常的代码
    result = 10 / 0
except ZeroDivisionError:
    # 处理特定类型异常的代码
    print("不能除以零!")
else:
    # 没有异常时执行的代码
    print("计算成功完成")
finally:
    # 无论是否发生异常都会执行的代码
    print("清理资源...")

1.2 常见的内置异常类型

以下是 Python 中一些最常见的内置异常类型,按类别组织:

基础异常

异常类型 描述 典型场景
Exception 常规异常的基类 自定义异常的基类
SystemExit sys.exit() 触发 程序正常退出
KeyboardInterrupt 用户按下中断键 用户按下 Ctrl+C

数据操作异常

异常类型 描述 典型场景
TypeError 类型不匹配 "2" + 2(字符串与整数相加)
ValueError 值不合适 int("abc")(非数字字符串转整数)
AttributeError 属性不存在 "hello".missing_method()
NameError 变量未定义 使用未定义的变量
IndexError 索引超出范围 my_list[100](列表越界)
KeyError 字典键不存在 my_dict["不存在的键"]

文件和 I/O 异常

异常类型 描述 典型场景
FileNotFoundError 文件不存在 open("不存在的文件.txt")
PermissionError 权限不足 写入只读文件
IsADirectoryError 是目录而非文件 尝试作为文件打开目录
TimeoutError 操作超时 网络连接超时

算术异常

异常类型 描述 典型场景
ZeroDivisionError 除以零 10 / 010 % 0
OverflowError 数值溢出 超大数运算
FloatingPointError 浮点数计算错误 浮点计算精度问题

其他重要异常

异常类型 描述 典型场景
ImportError 导入模块失败 import 不存在的模块
ModuleNotFoundError 找不到模块 模块路径错误
RuntimeError 一般运行时错误 运行时检测到的未分类错误
NotImplementedError 未实现的方法 抽象方法没有被子类实现
RecursionError 递归过深 无限递归或超出最大递归深度
SyntaxError 语法错误 代码不符合 Python 语法
IndentationError 缩进错误 不一致的缩进

2. 高级异常处理技术

2.1 异常链式传递

Python 3 引入了异常链式传递机制,使用 raise ... from ... 语法:

try:
    int("非数字")
except ValueError as e:
    raise RuntimeError("数据转换失败") from e

这种方式可以保留原始异常信息,同时提供更高级别的错误上下文。

2.2 自定义异常类

为特定应用场景创建自定义异常可以使代码更具表达力:

class ConfigError(Exception):
    """配置文件相关错误的基类"""
    pass

class ConfigFileNotFoundError(ConfigError):
    """配置文件不存在时触发"""
    pass

class ConfigParseError(ConfigError):
    """配置文件格式错误时触发"""
    pass

2.3 上下文管理器与异常处理

使用 with 语句可以简化资源管理和异常处理:

with open("data.txt", "r") as file:
    content = file.read()
    # 文件会自动关闭,即使发生异常

3. Python 单元测试基础

3.1 unittest 框架

unittest 是 Python 标准库中的单元测试框架:

import unittest

def add(a, b):
    return a + b

class TestAddFunction(unittest.TestCase):
    def test_add_integers(self):
        self.assertEqual(add(1, 2), 3)

    def test_add_floats(self):
        self.assertAlmostEqual(add(1.1, 2.2), 3.3)

    def test_add_strings(self):
        self.assertEqual(add("hello ", "world"), "hello world")

if __name__ == "__main__":
    unittest.main()

3.2 pytest 框架

pytest 是一个第三方测试框架,提供了更简洁的语法:

# test_add.py
def add(a, b):
    return a + b

def test_add_integers():
    assert add(1, 2) == 3

def test_add_floats():
    assert abs(add(1.1, 2.2) - 3.3) < 0.00001

def test_add_strings():
    assert add("hello ", "world") == "hello world"

运行测试只需命令 pytest test_add.py

3.3 常用断言方法

无论使用 unittest 还是 pytest,理解各种断言方法对编写有效的测试至关重要:

断言方法 用途 示例
assertEqual 判断两个值相等 self.assertEqual(1+1 2)
assertNotEqual 判断两个值不相等 self.assertNotEqual(1+1 3)
assertTrue 判断表达式为真 self.assertTrue(1 < 2)
assertFalse 判断表达式为假 self.assertFalse(1 > 2)
assertRaises 判断是否引发指定异常 self.assertRaises(ZeroDivisionError lambda: 1/0)
assertIn 判断元素在容器中 self.assertIn(1 [1 2 3])
assertNotIn 判断元素不在容器中 self.assertNotIn(4 [1 2 3])
assertIsNone 判断对象为 None self.assertIsNone(None)
assertIsNotNone 判断对象不为 None self.assertIsNotNone(0)

4. 错误处理与单元测试的结合

4.1 测试异常处理

import unittest

def divide(a, b):
    if b == 0:
        raise ValueError("被除数不能为零")
    return a / b

class TestDivideFunction(unittest.TestCase):
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5)

    def test_divide_zero(self):
        # 验证是否正确引发异常
        with self.assertRaises(ValueError) as context:
            divide(10, 0)
        # 验证异常消息
        self.assertIn("被除数不能为零", str(context.exception))

4.2 使用 mock 进行单元测试

from unittest.mock import Mock, patch
import unittest

def get_user_data(user_id):
    # 假设这个函数调用外部 API
    response = fetch_from_api(f"/users/{user_id}")
    return response.json()

class TestGetUserData(unittest.TestCase):
    @patch('__main__.fetch_from_api')
    def test_get_user_data(self, mock_fetch):
        # 创建一个模拟 response 对象
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "测试用户"}
        mock_fetch.return_value = mock_response

        # 测试函数
        result = get_user_data(1)

        # 验证结果
        self.assertEqual(result, {"id": 1, "name": "测试用户"})
        # 验证 fetch_from_api 被正确调用
        mock_fetch.assert_called_once_with("/users/1")

4.3 测试覆盖率

pip install coverage
coverage run -m pytest test_module.py
coverage report -m

这会生成一个测试覆盖率报告,显示每个模块的覆盖率以及未被测试覆盖的代码行。