[Python教程系列-05] 异常处理机制:编写健壮的Python程序

89 阅读14分钟

引言

在编程过程中,错误是不可避免的。无论是用户输入错误、文件不存在、网络连接失败,还是其他各种意外情况,都可能导致程序崩溃。如果我们不对这些错误进行处理,程序就会异常终止,给用户带来糟糕的体验。

Python提供了一套完善的异常处理机制,允许我们优雅地处理程序运行过程中可能出现的各种错误情况。通过合理的异常处理,我们可以让程序在遇到错误时不会崩溃,而是能够给出友好的提示信息,甚至自动恢复执行。

在本章中,我们将深入学习Python的异常处理机制,包括异常的类型、try-except语句的使用、自定义异常、finally语句等核心概念。通过实际的例子,你将学会如何编写更加健壮和用户友好的程序。

学习目标

完成本章学习后,你将能够:

  1. 理解异常的概念及其在程序中的作用
  2. 掌握try-except语句的基本用法
  3. 理解不同类型的内置异常及其用途
  4. 学会使用else和finally子句完善异常处理
  5. 掌握自定义异常的创建和使用方法
  6. 理解异常传播机制和处理策略
  7. 编写能够优雅处理错误的健壮程序
  8. 学会调试和分析异常信息

核心知识点讲解

什么是异常?

异常是程序执行过程中发生的错误或异常情况。当Python解释器遇到无法正常执行的代码时,就会抛出(raise)一个异常。如果我们不处理这些异常,程序就会终止并显示错误信息。

# 这会导致ZeroDivisionError异常
# result = 10 / 0

# 这会导致NameError异常
# print(undefined_variable)

# 这会导致TypeError异常
# result = "hello" + 5

常见的内置异常类型

Python提供了许多内置的异常类型,以下是几种最常见的:

  1. SyntaxError:语法错误
  2. NameError:尝试访问未定义的变量
  3. TypeError:类型错误,如对不支持该操作的数据类型执行操作
  4. ValueError:值错误,如将字符串"abc"转换为整数
  5. IndexError:索引错误,如访问列表不存在的索引
  6. KeyError:键错误,如访问字典不存在的键
  7. FileNotFoundError:文件未找到错误
  8. ZeroDivisionError:除零错误
  9. AttributeError:属性错误,如访问对象不存在的属性
  10. ImportError:导入错误

try-except语句

try-except语句是Python中处理异常的基本结构:

try:
    # 可能出现异常的代码
    risky_code()
except ExceptionType:
    # 处理特定异常的代码
    handle_exception()

基本用法

# 处理除零错误
try:
    result = 10 / 0
except ZeroDivisionError:
    print("错误:不能除以零")

# 处理值错误
try:
    number = int("abc")
except ValueError:
    print("错误:无法将字符串转换为整数")

处理多种异常

# 方法1:分别处理不同异常
try:
    value = int(input("请输入一个数字: "))
    result = 10 / value
    print(f"结果: {result}")
except ValueError:
    print("错误:请输入有效的数字")
except ZeroDivisionError:
    print("错误:不能除以零")

# 方法2:同时处理多种异常
try:
    value = int(input("请输入一个数字: "))
    result = 10 / value
    print(f"结果: {result}")
except (ValueError, ZeroDivisionError) as e:
    print(f"发生错误: {e}")

捕获所有异常

try:
    # 一些可能出错的代码
    risky_operation()
except Exception as e:
    print(f"发生了未知错误: {e}")
    print(f"错误类型: {type(e).__name__}")

else子句

else子句在try块没有引发异常时执行:

try:
    file = open("data.txt", "r")
except FileNotFoundError:
    print("文件未找到")
else:
    # 只有在没有异常时才执行
    content = file.read()
    print("文件内容:", content)
    file.close()

finally子句

finally子句无论是否发生异常都会执行,通常用于清理资源:

try:
    file = open("data.txt", "r")
    content = file.read()
    print(content)
except FileNotFoundError:
    print("文件未找到")
finally:
    # 无论是否发生异常都会执行
    try:
        file.close()
        print("文件已关闭")
    except:
        pass  # 如果文件未打开,close会引发异常

完整的异常处理结构

try:
    # 可能出现异常的代码
    pass
except SpecificException as e:
    # 处理特定异常
    pass
except Exception as e:
    # 处理其他所有异常
    pass
else:
    # 没有异常时执行
    pass
finally:
    # 无论如何都执行
    pass

抛出异常

我们可以使用raise语句主动抛出异常:

def validate_age(age):
    if age < 0:
        raise ValueError("年龄不能为负数")
    elif age > 150:
        raise ValueError("年龄不能超过150岁")
    return True

try:
    validate_age(-5)
except ValueError as e:
    print(f"验证失败: {e}")

自定义异常

通过继承Exception类,我们可以创建自定义异常:

class CustomError(Exception):
    """自定义异常基类"""
    pass

class AgeValidationError(CustomError):
    """年龄验证错误"""
    def __init__(self, age, message="年龄验证失败"):
        self.age = age
        self.message = message
        super().__init__(self.message)
    
    def __str__(self):
        return f"{self.message}: {self.age}"

class InsufficientFundsError(CustomError):
    """资金不足错误"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__("账户余额不足")
    
    def __str__(self):
        return f"余额: {self.balance}, 尝试提取: {self.amount}"

# 使用自定义异常
def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount

try:
    new_balance = withdraw(100, 150)
except InsufficientFundsError as e:
    print(e)  # 输出: 余额: 100, 尝试提取: 150

异常链

Python支持异常链,可以保留原始异常信息:

def process_data(data):
    try:
        return int(data) * 2
    except ValueError as e:
        # 重新抛出异常,保留原始异常信息
        raise TypeError("数据处理失败") from e

try:
    result = process_data("abc")
except TypeError as e:
    print(f"类型错误: {e}")
    print(f"原始异常: {e.__cause__}")

断言(assert)

assert语句用于调试,当条件为False时抛出AssertionError:

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

try:
    result = divide(10, 0)
except AssertionError as e:
    print(f"断言失败: {e}")

上下文管理器和with语句

with语句提供了一种更优雅的资源管理方式:

# 传统方式
try:
    file = open("data.txt", "r")
    content = file.read()
    print(content)
finally:
    file.close()

# 使用with语句(推荐)
with open("data.txt", "r") as file:
    content = file.read()
    print(content)
# 文件会自动关闭,即使发生异常也是如此

代码示例与实战

示例1:安全的文件操作工具

import os
import json
from typing import Any, Dict, List

class FileOperationError(Exception):
    """文件操作自定义异常"""
    pass

class SecureFileManager:
    """安全的文件管理器"""
    
    @staticmethod
    def read_text_file(filename: str) -> str:
        """安全读取文本文件"""
        try:
            with open(filename, 'r', encoding='utf-8') as file:
                return file.read()
        except FileNotFoundError:
            raise FileOperationError(f"文件 '{filename}' 未找到")
        except PermissionError:
            raise FileOperationError(f"没有权限读取文件 '{filename}'")
        except UnicodeDecodeError:
            raise FileOperationError(f"文件 '{filename}' 编码格式不正确")
        except Exception as e:
            raise FileOperationError(f"读取文件时发生未知错误: {e}")
    
    @staticmethod
    def write_text_file(filename: str, content: str) -> bool:
        """安全写入文本文件"""
        try:
            # 确保目录存在
            directory = os.path.dirname(filename)
            if directory and not os.path.exists(directory):
                os.makedirs(directory)
            
            with open(filename, 'w', encoding='utf-8') as file:
                file.write(content)
            return True
        except PermissionError:
            raise FileOperationError(f"没有权限写入文件 '{filename}'")
        except OSError as e:
            raise FileOperationError(f"写入文件时发生系统错误: {e}")
        except Exception as e:
            raise FileOperationError(f"写入文件时发生未知错误: {e}")
    
    @staticmethod
    def read_json_file(filename: str) -> Dict[str, Any]:
        """安全读取JSON文件"""
        try:
            with open(filename, 'r', encoding='utf-8') as file:
                return json.load(file)
        except FileNotFoundError:
            raise FileOperationError(f"JSON文件 '{filename}' 未找到")
        except json.JSONDecodeError as e:
            raise FileOperationError(f"JSON文件格式错误: {e}")
        except Exception as e:
            raise FileOperationError(f"读取JSON文件时发生未知错误: {e}")
    
    @staticmethod
    def write_json_file(filename: str, data: Dict[str, Any]) -> bool:
        """安全写入JSON文件"""
        try:
            # 确保目录存在
            directory = os.path.dirname(filename)
            if directory and not os.path.exists(directory):
                os.makedirs(directory)
            
            with open(filename, 'w', encoding='utf-8') as file:
                json.dump(data, file, ensure_ascii=False, indent=2)
            return True
        except TypeError as e:
            raise FileOperationError(f"数据无法序列化为JSON: {e}")
        except PermissionError:
            raise FileOperationError(f"没有权限写入文件 '{filename}'")
        except Exception as e:
            raise FileOperationError(f"写入JSON文件时发生未知错误: {e}")

def main():
    fm = SecureFileManager()
    
    # 测试文本文件操作
    try:
        # 写入文件
        content = "Hello, World!\n这是测试内容。"
        fm.write_text_file("test.txt", content)
        print("文本文件写入成功")
        
        # 读取文件
        read_content = fm.read_text_file("test.txt")
        print(f"读取的文本内容:\n{read_content}")
        
    except FileOperationError as e:
        print(f"文件操作失败: {e}")
    
    # 测试JSON文件操作
    try:
        # 准备测试数据
        test_data = {
            "name": "张三",
            "age": 25,
            "skills": ["Python", "Java", "JavaScript"],
            "address": {
                "city": "北京",
                "district": "朝阳区"
            }
        }
        
        # 写入JSON文件
        fm.write_json_file("test.json", test_data)
        print("JSON文件写入成功")
        
        # 读取JSON文件
        read_data = fm.read_json_file("test.json")
        print(f"读取的JSON数据: {read_data}")
        
    except FileOperationError as e:
        print(f"JSON文件操作失败: {e}")
    
    # 测试错误处理
    try:
        fm.read_text_file("nonexistent.txt")
    except FileOperationError as e:
        print(f"预期的错误处理: {e}")

if __name__ == "__main__":
    main()

示例2:网络请求异常处理

import time
import random
from typing import Optional, Dict, Any

class NetworkError(Exception):
    """网络错误"""
    pass

class HTTPError(NetworkError):
    """HTTP错误"""
    def __init__(self, status_code: int, message: str = ""):
        self.status_code = status_code
        self.message = message
        super().__init__(f"HTTP {status_code}: {message}")

class TimeoutError(NetworkError):
    """超时错误"""
    pass

class APIClient:
    """模拟API客户端"""
    
    def __init__(self, base_url: str, timeout: int = 30):
        self.base_url = base_url
        self.timeout = timeout
    
    def _simulate_request(self, endpoint: str) -> Dict[str, Any]:
        """模拟网络请求"""
        # 模拟网络延迟
        time.sleep(random.uniform(0.1, 0.5))
        
        # 模拟不同类型的错误
        error_type = random.choice(['success', 'http_error', 'timeout', 'network_error'])
        
        if error_type == 'success':
            return {
                "status": "success",
                "data": {
                    "endpoint": endpoint,
                    "timestamp": time.time(),
                    "result": f"Data from {endpoint}"
                }
            }
        elif error_type == 'http_error':
            raise HTTPError(404, "资源未找到")
        elif error_type == 'timeout':
            raise TimeoutError("请求超时")
        else:
            raise NetworkError("网络连接失败")
    
    def get(self, endpoint: str, retries: int = 3) -> Optional[Dict[str, Any]]:
        """带重试机制的GET请求"""
        last_exception = None
        
        for attempt in range(retries + 1):
            try:
                print(f"尝试请求 {endpoint} (第{attempt + 1}次)")
                response = self._simulate_request(endpoint)
                print(f"请求成功: {endpoint}")
                return response
                
            except HTTPError as e:
                # HTTP错误通常不需要重试
                print(f"HTTP错误: {e}")
                raise
                
            except TimeoutError as e:
                last_exception = e
                print(f"超时错误: {e}")
                if attempt < retries:
                    wait_time = 2 ** attempt  # 指数退避
                    print(f"等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)
                    
            except NetworkError as e:
                last_exception = e
                print(f"网络错误: {e}")
                if attempt < retries:
                    wait_time = 1
                    print(f"等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)
                    
            except Exception as e:
                last_exception = e
                print(f"未知错误: {e}")
                if attempt < retries:
                    wait_time = 1
                    print(f"等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)
        
        # 所有重试都失败了
        print(f"所有重试都失败了,最后一次错误: {last_exception}")
        raise last_exception

def main():
    client = APIClient("https://api.example.com")
    
    endpoints = ["/users", "/products", "/orders", "/invalid"]
    
    for endpoint in endpoints:
        try:
            response = client.get(endpoint, retries=2)
            if response:
                print(f"收到响应: {response['data']['result']}\n")
                
        except HTTPError as e:
            print(f"处理HTTP错误: {e}\n")
            
        except TimeoutError as e:
            print(f"处理超时错误: {e}\n")
            
        except NetworkError as e:
            print(f"处理网络错误: {e}\n")
            
        except Exception as e:
            print(f"处理未知错误: {e}\n")

if __name__ == "__main__":
    main()

示例3:用户输入验证系统

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

class UserInputValidator:
    """用户输入验证器"""
    
    @staticmethod
    def validate_email(email: str) -> str:
        """验证邮箱格式"""
        if not email:
            raise ValidationError("邮箱不能为空")
        
        if "@" not in email:
            raise ValidationError("邮箱格式不正确:缺少@符号")
        
        if "." not in email.split("@")[1]:
            raise ValidationError("邮箱格式不正确:域名部分缺少.符号")
        
        return email.lower().strip()
    
    @staticmethod
    def validate_phone(phone: str) -> str:
        """验证手机号格式"""
        if not phone:
            raise ValidationError("手机号不能为空")
        
        # 移除所有非数字字符
        digits = ''.join(filter(str.isdigit, phone))
        
        if len(digits) != 11:
            raise ValidationError("手机号必须是11位数字")
        
        if not digits.startswith('1'):
            raise ValidationError("手机号必须以1开头")
        
        return digits
    
    @staticmethod
    def validate_password(password: str) -> str:
        """验证密码强度"""
        if not password:
            raise ValidationError("密码不能为空")
        
        if len(password) < 8:
            raise ValidationError("密码长度至少8位")
        
        if not any(c.isupper() for c in password):
            raise ValidationError("密码必须包含至少一个大写字母")
        
        if not any(c.islower() for c in password):
            raise ValidationError("密码必须包含至少一个小写字母")
        
        if not any(c.isdigit() for c in password):
            raise ValidationError("密码必须包含至少一个数字")
        
        return password
    
    @staticmethod
    def validate_age(age_str: str) -> int:
        """验证年龄"""
        try:
            age = int(age_str)
        except ValueError:
            raise ValidationError("年龄必须是数字")
        
        if age < 0:
            raise ValidationError("年龄不能为负数")
        
        if age > 150:
            raise ValidationError("年龄不能超过150岁")
        
        return age

class RegistrationSystem:
    """注册系统"""
    
    def __init__(self):
        self.users = []
    
    def register_user(self):
        """用户注册"""
        print("=== 用户注册 ===")
        
        try:
            # 获取并验证邮箱
            email = input("请输入邮箱: ")
            validated_email = UserInputValidator.validate_email(email)
            print(f"✓ 邮箱验证通过: {validated_email}")
            
            # 获取并验证手机号
            phone = input("请输入手机号: ")
            validated_phone = UserInputValidator.validate_phone(phone)
            print(f"✓ 手机号验证通过: {validated_phone}")
            
            # 获取并验证密码
            password = input("请输入密码: ")
            validated_password = UserInputValidator.validate_password(password)
            print("✓ 密码验证通过")
            
            # 获取并验证年龄
            age_str = input("请输入年龄: ")
            validated_age = UserInputValidator.validate_age(age_str)
            print(f"✓ 年龄验证通过: {validated_age}")
            
            # 创建用户
            user = {
                "email": validated_email,
                "phone": validated_phone,
                "password": validated_password,  # 实际应用中应该加密存储
                "age": validated_age
            }
            
            self.users.append(user)
            print("✓ 用户注册成功!")
            return user
            
        except ValidationError as e:
            print(f"✗ 验证失败: {e}")
            return None
        except KeyboardInterrupt:
            print("\n✗ 用户取消注册")
            return None
        except Exception as e:
            print(f"✗ 注册过程中发生未知错误: {e}")
            return None
    
    def display_users(self):
        """显示所有用户"""
        if not self.users:
            print("暂无注册用户")
            return
        
        print("\n=== 注册用户列表 ===")
        for i, user in enumerate(self.users, 1):
            print(f"{i}. 邮箱: {user['email']}")
            print(f"   手机: {user['phone']}")
            print(f"   年龄: {user['age']}")
            print("-" * 30)

def main():
    system = RegistrationSystem()
    
    while True:
        print("\n=== 主菜单 ===")
        print("1. 用户注册")
        print("2. 查看用户列表")
        print("3. 退出")
        
        try:
            choice = input("请选择操作 (1-3): ").strip()
            
            if choice == "1":
                system.register_user()
            elif choice == "2":
                system.display_users()
            elif choice == "3":
                print("谢谢使用!")
                break
            else:
                print("无效选择,请输入1-3之间的数字")
                
        except KeyboardInterrupt:
            print("\n\n程序被用户中断")
            break
        except EOFError:
            print("\n\n输入结束")
            break
        except Exception as e:
            print(f"程序运行出错: {e}")

if __name__ == "__main__":
    main()

小结与回顾

在本章中,我们深入学习了Python的异常处理机制:

  1. 异常基础

    • 理解了异常的概念及其在程序中的作用
    • 掌握了常见的内置异常类型
  2. try-except语句

    • 学会了基本的异常捕获方法
    • 掌握了处理多种异常的技术
    • 理解了else和finally子句的用途
  3. 异常处理进阶

    • 学会了如何主动抛出异常
    • 掌握了自定义异常的创建和使用
    • 理解了异常链和上下文管理器
  4. 实际应用

    • 通过文件操作、网络请求、用户输入验证等实例,学会了在实际项目中应用异常处理

通过合理的异常处理,我们可以让程序更加健壮和用户友好。异常处理不仅是技术问题,更是用户体验问题。良好的异常处理能够让程序在出现问题时给出清晰的提示,而不是直接崩溃。

在下一章中,我们将学习文件操作,包括文本文件和二进制文件的读写,这将帮助我们处理持久化数据。

练习与挑战

基础练习

  1. 编写一个程序,让用户输入两个数字并进行除法运算,使用异常处理来捕获除零错误和值错误。
  2. 创建一个函数,接受一个列表和索引,返回对应元素,使用异常处理来处理索引越界的情况。
  3. 实现一个安全的字典访问函数,当键不存在时返回默认值而不是抛出KeyError。
  4. 编写一个程序,尝试打开一个文件并读取内容,使用异常处理来处理文件不存在的情况。

进阶挑战

  1. 设计一个完整的配置文件管理系统,能够安全地读取、写入和验证配置文件,包含完整的异常处理机制。
  2. 创建一个网络爬虫框架,包含重试机制、超时处理、错误日志记录等异常处理功能。
  3. 实现一个数据库连接池,包含连接失败处理、超时处理、连接回收等异常处理机制。
  4. 编写一个命令行工具,包含参数解析、用户输入验证、操作执行等完整的异常处理流程。

思考题

  1. 在什么情况下应该捕获异常,什么情况下应该让异常向上抛出?
  2. 如何设计良好的自定义异常层次结构?
  3. else子句和finally子句在异常处理中有什么不同作用?
  4. 什么时候应该使用断言(assert),什么时候应该使用异常处理?

扩展阅读

  1. Python官方文档 - 错误和异常 - 官方文档中关于异常处理的详细介绍
  2. Python官方文档 - 内置异常 - 所有内置异常类型的详细说明
  3. Python官方文档 - with语句 - 上下文管理器和with语句的详细说明
  4. 《流畅的Python》- 深入理解Python异常处理机制的经典书籍
  5. Real Python - Python Exceptions - 关于Python异常处理的详细教程
  6. PEP 341 - Unifying try-except and try-finally - Python异常处理语法的发展历史

通过本章的学习,你应该已经掌握了Python异常处理机制的核心概念和使用方法。这些知识将帮助你编写更加健壮和可靠的程序。在下一章中,我们将学习文件操作,包括文本文件和二进制文件的读写。