MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python自定义异常类

2021-09-074.7k 阅读

Python自定义异常类基础概念

在Python编程中,异常处理是确保程序稳健运行的重要机制。当程序遇到错误或异常情况时,Python会引发异常。默认情况下,Python提供了一系列内置异常类,比如ZeroDivisionError(除零错误)、FileNotFoundError(文件未找到错误)等。然而,在实际的复杂项目开发中,内置异常类往往无法精准地描述项目特定的错误场景。这时候,自定义异常类就派上用场了。

自定义异常类允许开发者根据项目需求,创建能够准确反映程序中特定错误类型的异常。通过自定义异常,代码的可读性和维护性会得到显著提升,因为异常信息能够更清晰地传达错误发生的原因和上下文。

从本质上讲,自定义异常类就是从Python内置的Exception类或其子类派生出来的新类。这样,新的异常类就继承了Exception类的所有属性和方法,同时开发者可以根据需求为其添加特定的属性和方法。

创建简单的自定义异常类

基本语法

创建自定义异常类非常简单,只需定义一个新类并让它继承自Exception类。以下是一个最基础的自定义异常类示例:

class MyCustomError(Exception):
    pass

在上述代码中,MyCustomError是我们定义的自定义异常类,它继承自Exception类。pass语句在这里作为占位符,因为目前这个自定义异常类没有额外的属性或方法。

使用自定义异常类

定义好自定义异常类后,就可以在代码中引发(raise)这个异常。例如:

class MyCustomError(Exception):
    pass

def divide_numbers(a, b):
    if b == 0:
        raise MyCustomError("不能除以零")
    return a / b

try:
    result = divide_numbers(10, 0)
except MyCustomError as e:
    print(f"捕获到自定义异常: {e}")

在这个示例中,divide_numbers函数在除数为零时引发MyCustomError异常。try - except块用于捕获这个异常,并打印出异常信息。

自定义异常类添加属性和方法

添加属性

为了让自定义异常类能够携带更多有用的信息,可以给它添加属性。例如,假设我们正在开发一个处理文件操作的模块,我们可能希望在文件权限不足时引发一个自定义异常,并在异常中包含文件名和缺少的权限信息。

class FilePermissionError(Exception):
    def __init__(self, filename, missing_permission):
        self.filename = filename
        self.missing_permission = missing_permission
        super().__init__(f"文件 {filename} 缺少 {missing_permission} 权限")


def read_file(file_path):
    import os
    if not os.access(file_path, os.R_OK):
        raise FilePermissionError(file_path, '读取')
    with open(file_path, 'r') as f:
        return f.read()


try:
    content = read_file('protected_file.txt')
    print(content)
except FilePermissionError as e:
    print(f"捕获到文件权限异常: {e}")
    print(f"文件名: {e.filename}")
    print(f"缺少的权限: {e.missing_permission}")

在上述代码中,FilePermissionError类的__init__方法接受文件名和缺少的权限作为参数,并将它们存储为实例属性。同时,通过调用super().__init__方法,将异常信息传递给父类Exception

添加方法

除了属性,自定义异常类还可以添加方法,以便在捕获异常时提供更多的操作。例如,我们可以为自定义异常类添加一个方法,用于生成一个包含错误详细信息的报告。

class DatabaseConnectionError(Exception):
    def __init__(self, host, port, error_message):
        self.host = host
        self.port = port
        self.error_message = error_message
        super().__init__(f"连接数据库 {host}:{port} 时出错: {error_message}")

    def generate_report(self):
        report = f"""
        数据库连接错误报告
        主机: {self.host}
        端口: {self.port}
        错误信息: {self.error_message}
        """
        return report


def connect_to_database(host, port):
    # 这里只是模拟连接失败的情况
    if host == 'localhost' and port == 5432:
        raise DatabaseConnectionError(host, port, '拒绝连接')
    return "数据库连接成功"


try:
    connection_status = connect_to_database('localhost', 5432)
    print(connection_status)
except DatabaseConnectionError as e:
    print(f"捕获到数据库连接异常: {e}")
    report = e.generate_report()
    print(report)

在这个例子中,DatabaseConnectionError类添加了一个generate_report方法,用于生成详细的错误报告。捕获异常后,我们调用这个方法并打印出报告。

自定义异常类的继承体系

多层继承

在大型项目中,可能需要创建一个异常类的继承体系,以便更好地组织和处理不同类型的异常。例如,假设我们正在开发一个游戏引擎,可能有一个基础的GameError异常类,然后从它派生出GraphicsErrorAudioError等子类,而GraphicsError又可以进一步派生出TextureLoadErrorShaderCompileError等更具体的异常类。

class GameError(Exception):
    pass


class GraphicsError(GameError):
    pass


class TextureLoadError(GraphicsError):
    def __init__(self, texture_name):
        self.texture_name = texture_name
        super().__init__(f"加载纹理 {texture_name} 时出错")


class ShaderCompileError(GraphicsError):
    def __init__(self, shader_name):
        self.shader_name = shader_name
        super().__init__(f"编译着色器 {shader_name} 时出错")


def load_texture(texture_name):
    if texture_name == 'missing_texture':
        raise TextureLoadError(texture_name)
    return "纹理加载成功"


def compile_shader(shader_name):
    if shader_name == 'broken_shader':
        raise ShaderCompileError(shader_name)
    return "着色器编译成功"


try:
    texture_status = load_texture('missing_texture')
    print(texture_status)
    shader_status = compile_shader('broken_shader')
    print(shader_status)
except TextureLoadError as e:
    print(f"捕获到纹理加载异常: {e}")
except ShaderCompileError as e:
    print(f"捕获到着色器编译异常: {e}")
except GraphicsError as e:
    print(f"捕获到图形相关异常: {e}")
except GameError as e:
    print(f"捕获到游戏相关异常: {e}")

在这个代码示例中,GameError是所有游戏相关异常的基类。GraphicsError继承自GameError,而TextureLoadErrorShaderCompileError又继承自GraphicsError。这种继承体系使得我们可以根据异常的具体类型进行更细粒度的捕获,同时也可以通过捕获基类异常来处理更宽泛的错误情况。

异常捕获的顺序

在处理异常继承体系时,异常捕获的顺序非常重要。Python会按照try - except块中except子句的顺序依次检查异常类型。如果先捕获了基类异常,那么子类异常将永远不会被捕获到,因为子类异常也是基类异常的实例。例如,在上述代码中,如果将except GameError as e:放在最前面,那么except TextureLoadError as e:except ShaderCompileError as e:将永远不会被执行。因此,应该先捕获最具体的异常类,然后再捕获更通用的基类异常。

自定义异常类在模块和包中的应用

模块内的自定义异常

在一个Python模块中,自定义异常类通常用于处理模块内部特定的错误情况。例如,假设我们有一个模块用于处理数学计算,其中可能会出现一些特定的数学错误,如矩阵维度不匹配。

# math_operations.py
class MatrixDimensionError(Exception):
    def __init__(self, matrix1, matrix2):
        self.matrix1 = matrix1
        self.matrix2 = matrix2
        super().__init__(f"矩阵维度不匹配: {matrix1} 和 {matrix2}")


def multiply_matrices(matrix1, matrix2):
    if len(matrix1[0]) != len(matrix2):
        raise MatrixDimensionError(matrix1, matrix2)
    result = []
    for i in range(len(matrix1)):
        row = []
        for j in range(len(matrix2[0])):
            sum_val = 0
            for k in range(len(matrix2)):
                sum_val += matrix1[i][k] * matrix2[k][j]
            row.append(sum_val)
        result.append(row)
    return result


在这个模块中,MatrixDimensionError是一个自定义异常类,用于处理矩阵乘法时维度不匹配的错误。multiply_matrices函数在检测到维度不匹配时会引发这个异常。

包中的自定义异常

当项目规模扩大,使用包来组织代码时,自定义异常类可以在包的不同模块之间共享。例如,假设我们有一个名为my_package的包,其中包含多个模块,用于处理数据处理和分析。我们可以在包的根目录下定义一个exceptions.py文件来存放所有相关的自定义异常类。

# my_package/exceptions.py
class DataProcessingError(Exception):
    pass


class DataFormatError(DataProcessingError):
    def __init__(self, data):
        self.data = data
        super().__init__(f"数据格式错误: {data}")


class DataMissingError(DataProcessingError):
    def __init__(self, variable):
        self.variable = variable
        super().__init__(f"数据缺失错误: 变量 {variable} 缺失")


然后在包内的其他模块中可以导入并使用这些自定义异常类。

# my_package/data_loader.py
from my_package.exceptions import DataFormatError, DataMissingError


def load_data(file_path):
    import json
    try:
        with open(file_path, 'r') as f:
            data = json.load(f)
        if 'required_variable' not in data:
            raise DataMissingError('required_variable')
        if not isinstance(data['value'], int):
            raise DataFormatError(data['value'])
        return data
    except FileNotFoundError:
        raise DataMissingError('文件')


在这个例子中,data_loader.py模块从my_package.exceptions导入了DataFormatErrorDataMissingError,并在load_data函数中使用它们来处理数据加载过程中的特定错误。这种方式使得包内的异常处理具有一致性和可维护性。

自定义异常类与日志记录

记录异常信息到日志文件

在实际项目中,捕获异常后不仅要处理异常,还需要记录异常信息以便后续调试和分析。Python的logging模块提供了强大的日志记录功能。结合自定义异常类,我们可以将异常信息详细地记录到日志文件中。

import logging


class MyBusinessError(Exception):
    def __init__(self, error_detail):
        self.error_detail = error_detail
        super().__init__(f"业务错误: {error_detail}")


def perform_business_operation():
    try:
        # 模拟业务操作失败
        raise MyBusinessError("关键业务步骤失败")
    except MyBusinessError as e:
        logging.basicConfig(filename='business_errors.log', level=logging.ERROR)
        logging.error(f"捕获到自定义异常: {e},详细信息: {e.error_detail}")


perform_business_operation()


在上述代码中,当MyBusinessError异常被捕获时,使用logging模块将异常信息和详细错误细节记录到business_errors.log文件中。通过配置logging.basicConfig,我们可以指定日志文件的名称和日志级别(这里设置为ERROR级别,只记录错误信息)。

日志格式与异常堆栈跟踪

logging模块还可以记录异常的堆栈跟踪信息,这对于定位错误发生的具体位置非常有帮助。例如:

import logging


class ComplexCalculationError(Exception):
    def __init__(self, operation):
        self.operation = operation
        super().__init__(f"复杂计算错误: {operation}")


def complex_calculation():
    try:
        # 模拟复杂计算失败
        raise ComplexCalculationError("乘法运算")
    except ComplexCalculationError as e:
        logging.basicConfig(filename='calculation_errors.log', level=logging.ERROR,
                            format='%(asctime)s - %(levelname)s - %(message)s - %(exc_info)s')
        logging.error(f"捕获到自定义异常: {e},操作: {e.operation}")


complex_calculation()


在这个例子中,logging.basicConfigformat参数中包含了%(exc_info)s,这会导致日志记录中包含异常的堆栈跟踪信息。这样,开发人员在查看日志时可以清楚地了解异常发生时的函数调用栈,从而更容易找出问题所在。

自定义异常类与单元测试

测试自定义异常的引发

在编写单元测试时,确保自定义异常在适当的情况下被引发是非常重要的。Python的unittest模块提供了方便的方法来测试异常。例如,对于之前定义的MatrixDimensionError异常类和multiply_matrices函数:

import unittest
from math_operations import multiply_matrices, MatrixDimensionError


class TestMatrixMultiplication(unittest.TestCase):
    def test_matrix_dimension_error(self):
        matrix1 = [[1, 2], [3, 4]]
        matrix2 = [[5, 6, 7], [8, 9, 10]]
        with self.assertRaises(MatrixDimensionError):
            multiply_matrices(matrix1, matrix2)


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


在这个单元测试中,test_matrix_dimension_error方法使用self.assertRaises上下文管理器来测试multiply_matrices函数在矩阵维度不匹配时是否会引发MatrixDimensionError异常。如果函数没有引发预期的异常,测试将失败。

测试异常处理逻辑

除了测试异常的引发,还可以测试异常处理逻辑。例如,假设我们有一个函数在捕获自定义异常后会执行一些恢复操作,我们可以测试这些恢复操作是否正确执行。

import unittest


class MyRecoveryError(Exception):
    pass


def perform_operation_with_recovery():
    try:
        raise MyRecoveryError()
    except MyRecoveryError:
        # 模拟恢复操作
        return "恢复成功"
    return "操作正常"


class TestRecoveryLogic(unittest.TestCase):
    def test_recovery_logic(self):
        result = perform_operation_with_recovery()
        self.assertEqual(result, "恢复成功")


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


在这个例子中,test_recovery_logic方法测试perform_operation_with_recovery函数在捕获MyRecoveryError异常后是否正确执行了恢复操作并返回了预期的结果。通过单元测试,我们可以确保自定义异常相关的代码逻辑的正确性和健壮性。

自定义异常类在不同编程范式中的应用

面向对象编程中的自定义异常

在面向对象编程中,自定义异常类与类的封装、继承和多态特性紧密结合。例如,一个图形绘制类库可能有一个基类Shape,并从它派生出CircleRectangle等子类。在绘制图形的过程中,可能会出现一些特定的错误,如半径为负的Circle对象。我们可以定义一个自定义异常类InvalidShapeParameterError

class InvalidShapeParameterError(Exception):
    pass


class Shape:
    pass


class Circle(Shape):
    def __init__(self, radius):
        if radius < 0:
            raise InvalidShapeParameterError("半径不能为负")
        self.radius = radius


class Rectangle(Shape):
    def __init__(self, width, height):
        if width < 0 or height < 0:
            raise InvalidShapeParameterError("宽度和高度不能为负")
        self.width = width
        self.height = height


在这个面向对象的设计中,CircleRectangle类在初始化时会检查参数的有效性,如果参数无效则引发InvalidShapeParameterError异常。这种方式保证了对象状态的合法性,符合面向对象编程中封装和数据保护的原则。

函数式编程中的自定义异常

在函数式编程范式中,虽然更强调不可变数据和纯函数,但自定义异常仍然有其用武之地。例如,假设我们有一个函数用于对列表中的数字进行特定的数学转换,如果列表中包含非数字元素,就需要引发一个自定义异常。

class NonNumericElementError(Exception):
    pass


def transform_list(lst):
    result = []
    for element in lst:
        if not isinstance(element, (int, float)):
            raise NonNumericElementError(f"列表中包含非数字元素: {element}")
        result.append(element * 2)
    return result


在这个函数式编程风格的代码中,transform_list函数在处理列表元素时,如果遇到非数字元素,会引发NonNumericElementError异常。这样可以确保函数的输入符合预期,同时保持函数的纯函数特性(即相同的输入总是产生相同的输出,除非引发异常)。

自定义异常类的最佳实践

异常命名规范

自定义异常类的命名应该清晰明了,能够准确反映异常的含义。通常遵循驼峰命名法,并且以Error结尾,例如DatabaseConnectionErrorFilePermissionError等。这样的命名方式使得代码阅读者能够快速理解异常的类型和用途。

异常信息的完整性

在自定义异常类的__init__方法中,应该尽量提供完整的异常信息。异常信息应该包含足够的上下文,以便开发人员能够快速定位和解决问题。例如,对于文件相关的异常,应该包含文件名;对于数据库相关的异常,应该包含数据库连接信息等。

避免过度使用自定义异常

虽然自定义异常类非常有用,但也不应该过度使用。过度使用自定义异常可能会导致代码变得复杂,难以维护。只有在确实需要精确描述特定错误场景时,才使用自定义异常。对于一些常见的错误,应该优先使用Python内置的异常类。

异常处理的一致性

在整个项目中,异常处理的方式应该保持一致。无论是捕获自定义异常还是内置异常,都应该采用统一的策略,如记录日志、返回合适的错误信息给用户等。这样可以提高代码的可维护性和可读性。

通过深入理解和合理应用自定义异常类,Python开发者能够编写出更加健壮、可读和易于维护的代码,更好地应对各种复杂的编程场景。