Logo
Published on

3.13.异常

Authors
  • avatar
    Name
    xiaobai
    Twitter

1.概述

异常是程序运行时发生的错误事件,会中断正常的指令流。Python 提供了强大的异常处理机制,让程序能够优雅地处理错误情况,提高程序的健壮性和用户体验。

2.核心价值

  • 提高程序的健壮性:避免程序因错误而崩溃
  • 提供友好的错误信息:给用户清晰的错误提示
  • 确保资源正确释放:防止资源泄漏
  • 便于调试和维护:提供详细的错误信息

3.基本语法

3.1.try-except 结构

try:
    # 可能发生异常的代码块
    ...
except 异常类型 as 变量:
    # 异常发生时的处理代码
    ...

3.2.工作机制

  1. Python 解释器依次运行 try 块中的代码
  2. 一旦遇到错误(异常),立即跳转到匹配的 except 块处理
  3. 如果没有发生异常,则 except 块会跳过,不会执行
  4. 如果 try 块中的异常类型与 except 指定的不符,异常将继续向上传递

3.3.基本示例

try:
    x = int(input("请输入一个整数: "))
    y = 10 / x
    print("结果:", y)
except ValueError:
    print("输入的不是有效整数!")
except ZeroDivisionError:
    print("不能除以零!")

4.内置异常类型

4.1.异常层次结构

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── FloatingPointError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── TimeoutError
    ├── ValueError
    ├── TypeError
    ├── AttributeError
    ├── NameError
    └── RuntimeError

4.2.常见异常类型

异常类型描述示例
ValueError传入无效参数int('abc')
TypeError操作或函数应用的对象类型不正确'2' + 2
IndexError序列索引超出范围[1, 2, 3][10]
KeyError字典中查找不存在的键{'a': 1}['b']
AttributeError对象不存在属性或方法'hello'.nonexistent_method()
NameError未声明/未定义变量被访问print(undefined_variable)
FileNotFoundError尝试打开不存在的文件open('nonexistent_file.txt')
ZeroDivisionError除法或模运算中除数为零10 / 0

4.3.异常演示

exceptions = [
    ("10 / 0", ZeroDivisionError),
    ("int('abc')", ValueError),
    ("'2' + 2", TypeError),
    ("[1, 2, 3][10]", IndexError),
    ("{'a': 1}['b']", KeyError),
    ("'hello'.nonexistent_method()", AttributeError),
    ("print(undefined_variable)", NameError),
    ("open('nonexistent_file.txt')", FileNotFoundError)
]

for code, expected_exception in exceptions:
    try:
        exec(code)
    except expected_exception as e:
        print(f"{code:30} -> {type(e).__name__}: {e}")

5.抛出异常

5.1.使用 raise 语句

抛出异常指的是主动触发一个错误,让程序中断当前流程,并转交给异常处理机制。

5.1.1.基本语法

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

try:
    divide(10, 0)
except ZeroDivisionError as e:
    print("发生异常:", e)

5.1.2.raise 的用法

  • raise 异常类():抛出异常类的实例
  • raise 异常类("错误信息"):抛出带错误信息的异常实例
  • raise:在 except 块中重新抛出当前异常

5.1.3.常见场景

  • 数据校验:参数不合法时,主动抛出异常阻止非法操作
  • 接口规范:在自定义函数/类中,明确遇到非法状态或不支持的操作时抛出异常

5.2.重新抛出异常

重新抛出(re-raise)异常通常用于在捕获异常后执行一些操作(如日志、清理等),然后将异常继续向上传递

def process_data(data):
    try:
        number = int(data)
        result = 100 / number
        return result
    except ValueError:
        print("数据格式错误,记录日志...")
        raise  # 重新抛出原始异常
    except ZeroDivisionError:
        print("除零错误,记录日志...")
        raise  # 重新抛出原始异常

try:
    process_data("abc")
except Exception as e:
    print(f"外层捕获: {e}")

5.3.异常链

异常链(Exception Chaining)是指在处理一个异常时,又引发了新的异常,可以用 raise ... from ... 的语法将新的异常和原始异常串联起来。

5.3.1.语法

try:
    # 语句块,可能发生异常
except 原始异常 as e:
    # 记录日志、清理等
    raise 新异常("描述") from e  # 指定异常链

5.3.2.示例

def process_file(filename):
    try:
        with open(filename) as f:
            return f.read()
    except FileNotFoundError as e:
        # 封装为业务相关异常,链接原始异常
        raise RuntimeError("文件处理失败") from e

try:
    data = process_file("not_exist.txt")
except RuntimeError as err:
    print("捕获到业务异常:", err)
    print("原始异常为:", err.__cause__)

5.3.3.好处

  • 更好地定位和排查复杂业务异常的根源
  • 让异常日志和调用栈更加清晰,方便运维和开发调试

6.自定义异常

6.1.创建自定义异常类

自定义异常是指根据业务需要,从现有的异常基类(如Exception)派生出更加"特定语义"的异常类,用来表达程序中的特殊错误情况。

6.1.1.好处

  • 语义明确:异常名称直接表达了业务意图,便于定位和维护
  • 增强可读性:通过捕获自定义异常,代码逻辑清晰,易于管理不同错误
  • 便于分层处理:可以设计继承结构,精准处理某一类或所有子类异常

6.1.2.基本写法

class MyCustomError(Exception):
    """自定义异常:用于描述特定业务错误"""
    pass

6.1.3.带参数的异常

class DataFormatError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field} 格式错误: {message}")

# 使用示例
try:
    raise DataFormatError("email", "缺少@符号")
except DataFormatError as e:
    print(f"字段: {e.field}, 错误: {e.message}")

6.1.4.捕获自定义异常

try:
    # 某些操作,可能抛出自定义异常
    raise MyCustomError("Something went wrong!")
except MyCustomError as e:
    print("捕获到自定义异常:", e)

7.上下文管理器与异常

7.1.with 语句的异常处理

with 语句(上下文管理器)可以优雅地管理资源的获取与释放,并自动处理异常。

7.1.1.工作机制

  1. __enter__():进入 with 块时自动调用,用于获取资源
  2. with 块内部:执行用户代码,如果发生异常会被自动捕获
  3. __exit__(exc_type, exc_val, exc_tb):离开 with 块时自动调用,无论是否有异常发生

7.1.2.示例

class FileOpener:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        print("文件已打开")
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
            print("文件已关闭")
        if exc_type:  # 发生了异常
            print(f"发生异常:{exc_type.__name__}: {exc_val}")
        return False  # 返回False,异常不会被吞掉

try:
    with FileOpener("demo.txt", "w") as f:
        f.write("hello world\n")
        raise ValueError("人为制造错误测试异常处理")
        f.write("这句话不会执行")
except Exception as e:
    print("外部捕获到异常:", e)

7.1.3.输出效果

文件已打开
文件已关闭
发生异常:ValueError: 人为制造错误测试异常处理
外部捕获到异常: 人为制造错误测试异常处理

7.1.4.小结

  • with 语句中的上下文管理器,无论是否出错都能确保资源被正确释放
  • 可以在 __exit__ 方法中对异常进行统一处理和日志记录
  • 记得仅在异常能安全处理时返回 True,否则让异常继续抛出
  • 大量应用场景:文件操作、数据库连接、线程锁、网络通信等

8.最佳实践

8.1.1. 具体的异常捕获

# 不推荐 - 过于宽泛
try:
    # 一些操作
    pass
except:
    pass

# 推荐 - 具体异常
try:
    with open("data.txt", "r") as f:
        data = f.read()
except FileNotFoundError:
    print("文件不存在")
except PermissionError:
    print("没有文件权限")
except OSError as e:
    print(f"系统错误: {e}")

8.2.2. 避免空的 except 块

# 不推荐
try:
    risky_operation()
except:
    pass  # 静默忽略错误

# 推荐
try:
    risky_operation()
except SpecificError as e:
    logger.error(f"操作失败: {e}")
    # 或者采取恢复措施

8.3.3. 资源清理

# 不推荐
file = open("data.txt", "r")
try:
    data = file.read()
    process_data(data)
finally:
    file.close()

# 推荐 - 使用 with 语句
with open("data.txt", "r") as file:
    data = file.read()
    process_data(data)

8.4.4. 异常日志记录

import logging

# 配置日志模块
logging.basicConfig(level=logging.ERROR)

class DatabaseError(Exception):
    pass

class ValidationError(Exception):
    pass

def get_user_from_db(user_id):
    """根据用户ID从数据库获取用户信息"""
    if user_id == 0:
        raise DatabaseError("数据库连接失败")
    return {"user_id": user_id, "name": "Test User", "age": 25}

def validate_and_process(user_data):
    """验证用户数据的有效性"""
    if not user_data.get("name") or user_data.get("age") < 0:
        raise ValidationError("用户信息无效")
    user_data["processed"] = True
    return user_data

def process_user_data(user_id):
    """综合处理用户数据"""
    try:
        user_data = get_user_from_db(user_id)
        result = validate_and_process(user_data)
        return result
    except DatabaseError as e:
        logging.error(f"数据库错误 - 用户ID: {user_id}, 错误: {e}")
        raise
    except ValidationError as e:
        logging.warning(f"数据验证失败 - 用户ID: {user_id}, 错误: {e}")
        return None
    except Exception as e:
        logging.critical(f"未知错误 - 用户ID: {user_id}, 错误: {e}")
        raise

process_user_data(1)

9.高级异常技巧

9.1.异常组 (Python 3.11+)

异常组(Exception Group)是 Python 3.11 新引入的特性,用于在同一代码块中同时抛出多个异常。

9.1.1.基本语法

def multiple_operations():
    errors = []
    try:
        int("abc")
    except ValueError as e:
        errors.append(e)
    try:
        1 / 0
    except ZeroDivisionError as e:
        errors.append(e)
    if errors:
        raise ExceptionGroup("多个操作失败", errors)

# 使用 except* 语句分别处理不同类型的异常
try:
    multiple_operations()
except* ValueError as eg:
    print("处理ValueError:")
    for e in eg.exceptions:
        print(f"  - {e}")
except* ZeroDivisionError as eg:
    print("处理ZeroDivisionError:")
    for e in eg.exceptions:
        print(f"  - {e}")

注意:except* 语法仅支持在 Python 3.11 及以上版本使用,且只能用于处理异常组。

9.2.异常调试信息

import traceback

try:
    data = {"key": "value"}
    print(data["nonexistent_key"])
except Exception as e:
    print("错误信息:", str(e))
    print("错误类型:", type(e).__name__)
    print("追踪信息:")
    traceback.print_exc()  # 直接打印栈信息
    
    # 获取详细的堆栈信息
    tb_info = traceback.format_exc()
    print("格式化堆栈:")
    print(tb_info)

9.3.性能考虑

import time

# 方法1: 使用异常处理流程控制(不推荐)
def with_exceptions(n):
    results = []
    for i in range(n):
        try:
            if i % 2 == 0:
                raise ValueError("测试异常")
            results.append(i)
        except ValueError:
            pass
    return results

# 方法2: 使用条件判断(推荐)
def without_exceptions(n):
    results = []
    for i in range(n):
        if i % 2 != 0:
            results.append(i)
    return results

n = 10000

start = time.time()
with_exceptions(n)
exception_time = time.time() - start

start = time.time()
without_exceptions(n)
normal_time = time.time() - start

print(f"使用异常: {exception_time:.6f}秒")
print(f"使用条件: {normal_time:.6f}秒")
print(f"性能差异: {exception_time/normal_time:.2f}倍")

10.实际应用示例

10.1.Web 应用异常处理

from flask import Flask, jsonify

app = Flask(__name__)

# 定义自定义异常
class UserNotFoundError(Exception):
    pass

class InvalidInputError(Exception):
    pass

# 注册错误处理函数
@app.errorhandler(UserNotFoundError)
def handle_user_not_found(error):
    return jsonify({
        "error": "user_not_found",
        "message": "用户不存在"
    }), 404

@app.errorhandler(InvalidInputError)
def handle_invalid_input(error):
    return jsonify({
        "error": "invalid_input",
        "message": "输入数据无效"
    }), 400

@app.errorhandler(Exception)
def handle_generic_error(error):
    return jsonify({
        "error": "internal_server_error",
        "message": "服务器内部错误"
    }), 500

# 定义路由
@app.route('/users/<int:user_id>')
def get_user(user_id):
    if user_id <= 0:
        raise InvalidInputError("用户ID必须大于0")
    
    user = get_user_from_database(user_id)
    if not user:
        raise UserNotFoundError(f"用户 {user_id} 不存在")
    
    return jsonify(user)

def get_user_from_database(user_id):
    if user_id == 1:
        return {"id": 1, "name": "Alice"}
    return None

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

10.2.数据验证框架

class DataValidator:
    @staticmethod
    def validate_email(email):
        if not isinstance(email, str):
            raise TypeError("邮箱必须是字符串")
        if "@" not in email:
            raise ValueError("无效的邮箱格式")
        return email.lower()
    
    @staticmethod
    def validate_age(age):
        if not isinstance(age, int):
            raise TypeError("年龄必须是整数")
        if age < 0 or age > 150:
            raise ValueError("年龄必须在0-150之间")
        return age
    
    @staticmethod
    def validate_user_data(user_data):
        errors = {}
        
        try:
            user_data['email'] = DataValidator.validate_email(user_data.get('email'))
        except (TypeError, ValueError) as e:
            errors['email'] = str(e)
        
        try:
            user_data['age'] = DataValidator.validate_age(user_data.get('age'))
        except (TypeError, ValueError) as e:
            errors['age'] = str(e)
        
        if errors:
            raise ValidationError("数据验证失败", errors)
        
        return user_data

class ValidationError(Exception):
    def __init__(self, message, errors):
        self.message = message
        self.errors = errors
        super().__init__(self.message)

# 使用示例
user_input = {
    'email': 'invalid-email',
    'age': 200
}

try:
    validated_data = DataValidator.validate_user_data(user_input)
    print("数据验证成功:", validated_data)
except ValidationError as e:
    print(f"验证失败: {e.message}")
    for field, error in e.errors.items():
        print(f"  {field}: {error}")

11.总结

11.1.异常处理的核心原则

  1. 具体性: 捕获具体的异常类型
  2. 透明性: 不要静默忽略异常
  3. 资源管理: 使用 with 语句确保资源释放
  4. 层次性: 合理设计异常类层次结构
  5. 信息性: 提供有意义的错误信息

11.2.关键要点

  • 使用 try-except-else-finally 完整结构
  • 自定义异常提高代码可读性
  • 异常应用于异常情况,不要用于流程控制
  • 合理使用异常链和异常组
  • 记录异常信息便于调试

11.3.异常处理流程图

开始
执行代码
发生异常?
  ↓ 是
捕获异常
处理异常
继续执行
结束