Skip to content

9.1 常见异常类型认识

在 Python 编程中,异常是程序运行时出现的错误信号。Python 提供了丰富的内置异常类型,帮助我们精准定位问题。了解常见异常类型,就像医生熟悉常见病症一样,能让我们快速“对症下药”。

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
索引越界错误IndexError当尝试访问序列(如列表、元组)中不存在的索引时抛出
键错误KeyError当字典中不存在指定键时抛出
类型错误TypeError当操作或函数应用于不适当类型的对象时抛出
值错误ValueError当操作或函数接收到具有正确类型但不适当值的参数时抛出
零除错误ZeroDivisionError当除法或取模运算的第二个参数为零时抛出
python
# 演示常见异常类型

# 创建一个简单的列表用于演示
my_list = [1, 2, 3]

# 尝试访问不存在的索引(会引发 IndexError)
try:
    # 尝试获取索引为 10 的元素(列表只有 3 个元素)
    value = my_list[10]
except IndexError as e:
    # 捕获 IndexError 异常并打印错误信息
    print(f"索引错误: {e}")

# 创建一个简单的字典用于演示
my_dict = {"name": "Alice", "age": 25}

# 尝试访问不存在的键(会引发 KeyError)
try:
    # 尝试获取不存在的键 "address"
    address = my_dict["address"]
except KeyError as e:
    # 捕获 KeyError 异常并打印错误信息
    print(f"键错误: {e}")

# 类型错误演示(会引发 TypeError)
try:
    # 尝试将字符串和整数相加(不兼容的操作)
    result = "hello" + 5
except TypeError as e:
    # 捕获 TypeError 异常并打印错误信息
    print(f"类型错误: {e}")

# 值错误演示(会引发 ValueError)
try:
    # 尝试将非数字字符串转换为整数
    number = int("abc")
except ValueError as e:
    # 捕获 ValueError 异常并打印错误信息
    print(f"值错误: {e}")

# 零除错误演示(会引发 ZeroDivisionError)
try:
    # 尝试除以零
    result = 10 / 0
except ZeroDivisionError as e:
    # 捕获 ZeroDivisionError 异常并打印错误信息
    print(f"零除错误: {e}")

需要注意的是,所有异常类型都继承自 BaseException 类,而我们通常处理的异常都继承自 Exception 类。在实际编程中,我们应该尽量捕获具体的异常类型,而不是笼统地捕获所有异常,这样可以让错误处理更加精确和有针对性。

9.2 try-except 基本语法

try-except 是 Python 中处理异常的核心语法结构,它让我们能够优雅地处理程序中可能出现的错误,而不是让程序直接崩溃。这就像给程序穿上了一件防弹衣,即使遇到“子弹”(异常),也能继续正常运行。

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
基本异常捕获try-except捕获并处理指定类型的异常
多异常捕获try-except-except使用多个 except 子句捕获不同类型的异常
通用异常捕获try-except Exception捕获所有 Exception 子类异常(不推荐作为唯一捕获方式)
异常信息获取except Exception as e通过 as 关键字获取异常对象,便于调试
python
# try-except 基本语法演示

try:
    num = int(input("请输入一个整数:"))
    result = 10 / num
    print("结果是:", result)
except ValueError:
    print("输入的不是整数,请重新输入。")
except ZeroDivisionError:
    print("不能除以零。")
except Exception as e:   # 用于捕获其他意外异常,便于记录
    print("发生了未知错误:", e)

最佳实践

在实际程序中,应尽可能捕获具体的异常类型,以便准确地处理错误和定位问题。避免使用过于宽泛的 except Exception,除非你确实需要捕获所有意外异常并进行日志记录。

使用 try-except 时要注意,except 子句应该按照从具体到一般的顺序排列。如果先写通用异常处理器,后面的特定异常处理器就永远不会被执行。另外,不要为了消除异常而简单地使用 pass,这样会掩盖真正的问题。

9.3 finally 子句的作用

finally 子句是 try-except 结构中的"善后专家",无论是否发生异常,它都会执行。这在资源清理场景中特别有用,比如关闭文件、释放网络连接或数据库连接等。finally 就像是程序的"收尾工作",确保一切都能妥善处理。

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
资源清理try-except-finally确保资源被正确释放,无论是否发生异常
状态重置try-finally在操作完成后重置状态变量
日志记录try-except-finally记录操作完成状态,无论成功还是失败
python
# finally 子句作用演示

# 场景1:文件操作中的资源清理
def read_file_safely(filename):
    """
    安全读取文件内容
    参数 filename: 文件名
    返回: 文件内容或错误信息
    """
    file_handle = None
    try:
        # 打开文件
        file_handle = open(filename, 'r', encoding='utf-8')
        # 读取文件内容
        content = file_handle.read()
        return content
    except FileNotFoundError:
        # 文件未找到异常
        return "错误:文件不存在!"
    except IOError:
        # IO 操作异常
        return "错误:文件读取失败!"
    finally:
        # 无论是否发生异常,都要关闭文件
        if file_handle and not file_handle.closed:
            file_handle.close()
            print("文件已关闭")

# 注意:由于我们没有实际文件,这里只展示语法结构
# 在实际使用中,可以创建测试文件来验证

# 场景2:数据库连接模拟
class DatabaseConnection:
    """模拟数据库连接类"""
    def __init__(self):
        self.connected = False
    
    def connect(self):
        """建立连接"""
        self.connected = True
        print("数据库连接已建立")
    
    def disconnect(self):
        """断开连接"""
        self.connected = False
        print("数据库连接已关闭")
    
    def execute_query(self, query):
        """执行查询"""
        if not self.connected:
            raise ConnectionError("数据库未连接")
        if "invalid" in query.lower():
            raise ValueError("无效的查询语句")
        return f"查询结果: {query}"

def safe_database_operation(query):
    """
    安全的数据库操作
    参数 query: SQL 查询语句
    返回: 查询结果或错误信息
    """
    db = DatabaseConnection()
    try:
        # 建立数据库连接
        db.connect()
        # 执行查询
        result = db.execute_query(query)
        return result
    except ConnectionError as e:
        return f"连接错误: {e}"
    except ValueError as e:
        return f"查询错误: {e}"
    finally:
        # 确保数据库连接被关闭
        db.disconnect()

# 测试数据库操作
print(safe_database_operation("SELECT * FROM users"))      # 正常情况
print(safe_database_operation("INVALID QUERY"))           # 查询错误情况

# 场景3:计时器示例
import time

def timed_operation(operation_name):
    """
    带计时的操作执行
    参数 operation_name: 操作名称
    """
    start_time = time.time()
    try:
        print(f"开始执行: {operation_name}")
        # 模拟一些操作
        time.sleep(1)
        # 故意引发异常来测试 finally
        if operation_name == "risky":
            raise RuntimeError("操作失败")
        print("操作成功完成")
    except RuntimeError as e:
        print(f"操作失败: {e}")
    finally:
        end_time = time.time()
        elapsed_time = end_time - start_time
        print(f"操作 '{operation_name}' 耗时: {elapsed_time:.2f} 秒")

# 测试计时操作
timed_operation("normal")   # 正常操作
timed_operation("risky")    # 引发异常的操作

finally 子句的强大之处在于它的确定性——无论 try 块中发生了什么(正常执行、异常抛出、甚至是 return 语句),finally 块都会执行。这使得它成为资源管理的理想选择。不过要注意,在 finally 块中避免使用 return 语句,因为它会覆盖 try 或 except 块中的 return 值。

9.4 简单自定义异常

虽然 Python 提供了丰富的内置异常类型,但在实际开发中,我们经常需要定义自己的异常类型来更好地表达特定业务场景中的错误情况。自定义异常就像是为我们的应用程序量身定制的"错误语言",让错误信息更加清晰和有意义。

功能名称实例调用方法具体功能、注意事项、必需参数/可选参数
基础自定义异常class CustomError(Exception)继承 Exception 类创建自定义异常
带消息的自定义异常class CustomError(Exception): def init(self, message)自定义异常构造函数,支持自定义错误消息
带额外信息的异常class CustomError(Exception): def init(self, message, error_code)包含额外属性如错误码等
python
# 简单自定义异常演示

# 场景1:基础自定义异常
class InvalidAgeError(Exception):
    """年龄无效异常"""
    pass

def validate_age(age):
    """
    验证年龄是否有效
    参数 age: 年龄值
    返回: 验证通过返回 True,否则抛出 InvalidAgeError
    """
    if not isinstance(age, int):
        raise InvalidAgeError("年龄必须是整数")
    if age < 0:
        raise InvalidAgeError("年龄不能为负数")
    if age > 150:
        raise InvalidAgeError("年龄不能超过150岁")
    return True

# 测试年龄验证
try:
    validate_age(25)      # 正常情况
    print("年龄验证通过")
except InvalidAgeError as e:
    print(f"年龄验证失败: {e}")

try:
    validate_age(-5)      # 异常情况
except InvalidAgeError as e:
    print(f"年龄验证失败: {e}")

# 场景2:带自定义消息的异常
class InsufficientFundsError(Exception):
    """余额不足异常"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        # 构造详细的错误消息
        message = f"余额不足!当前余额: ${balance:.2f}, 尝试支出: ${amount:.2f}"
        super().__init__(message)

class BankAccount:
    """银行账户类"""
    def __init__(self, initial_balance=0):
        self.balance = initial_balance
    
    def withdraw(self, amount):
        """
        取款操作
        参数 amount: 取款金额
        """
        if amount > self.balance:
            # 抛出自定义异常,包含详细信息
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return self.balance

# 测试银行账户
account = BankAccount(100.0)
try:
    account.withdraw(50.0)   # 正常取款
    print(f"取款成功,余额: ${account.balance:.2f}")
    account.withdraw(200.0)  # 余额不足
except InsufficientFundsError as e:
    print(f"取款失败: {e}")
    print(f"错误详情 - 余额: ${e.balance:.2f}, 尝试取款: ${e.amount:.2f}")

# 场景3:业务逻辑异常
class ValidationError(Exception):
    """数据验证异常"""
    def __init__(self, field, value, reason):
        self.field = field
        self.value = value
        self.reason = reason
        message = f"字段 '{field}' 验证失败: 值 '{value}' - 原因: {reason}"
        super().__init__(message)

def validate_email(email):
    """
    验证邮箱格式
    参数 email: 邮箱地址
    返回: 验证通过返回 True,否则抛出 ValidationError
    """
    if not isinstance(email, str):
        raise ValidationError("email", email, "邮箱必须是字符串")
    if "@" not in email:
        raise ValidationError("email", email, "邮箱必须包含 @ 符号")
    if "." not in email.split("@")[1]:
        raise ValidationError("email", email, "域名必须包含点号")
    return True

# 测试邮箱验证
try:
    validate_email("user@example.com")  # 正常情况
    print("邮箱验证通过")
except ValidationError as e:
    print(f"邮箱验证失败: {e}")

try:
    validate_email("invalid-email")     # 异常情况
except ValidationError as e:
    print(f"邮箱验证失败: {e}")
    print(f"验证详情 - 字段: {e.field}, 值: {e.value}, 原因: {e.reason}")

自定义异常的最佳实践是继承适当的内置异常类(通常是 Exception 或其子类),并提供有意义的错误消息。通过在自定义异常中添加额外的属性,我们可以传递更多上下文信息,帮助调用者更好地理解和处理错误。记住,好的异常设计能让代码的错误处理更加清晰和可维护。