python让程序更健壮 – 异常处理:最佳实践

欢迎学习异常处理的”艺术”!

目前你已经掌握了异常处理的基本技能,让我们来学习如何优雅地使用它们。好的异常处理就像好的礼仪,让代码更加优雅和专业!

1. 具体异常优于通用异常

不好的做法:

try:
    # 各种操作
    file = open("data.txt", "r")
    number = int("123")
    result = 10 / 2
except Exception:  # 太宽泛!
    print("发生错误")

好的做法:

try:
    file = open("data.txt", "r")
    content = file.read()
    number = int(content)
    result = 10 / number
except FileNotFoundError:
    print("❌ 文件不存在")
except ValueError:
    print("❌ 文件内容不是有效数字")
except ZeroDivisionError:
    print("❌ 不能除以零")

2. 不要静默吞掉异常

不好的做法:

def get_config_value(key):
    try:
        return config[key]
    except KeyError:
        pass  # 静默失败!没人知道出错了

好的做法:

def get_config_value(key):
    try:
        return config[key]
    except KeyError:
        logging.warning(f"配置项 '{key}' 不存在,使用默认值")
        return DEFAULT_VALUES.get(key)
    # 或者重新抛出
    # raise ConfigurationError(f"缺少必要的配置项: {key}")

3. 使用上下文管理器自动清理资源

不好的做法:

try:
    file = open("data.txt", "r")
    # 处理文件
    content = file.read()
    file.close()  # 可能忘记调用
except IOError:
    if file:
        file.close()

好的做法:

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

4. 提供有意义的错误信息

不好的做法:

def calculate_bmi(weight, height):
    if height <= 0:
        raise ValueError("高度无效")

好的做法:

def calculate_bmi(weight, height):
    if height <= 0:
        raise ValueError(
            f"高度必须为正数,收到: {height}。"
            f"请检查单位是否正确(米为单位)。"
        )
    if weight <= 0:
        raise ValueError(
            f"体重必须为正数,收到: {weight}。"
            f"请检查单位是否正确(千克为单位)。"
        )
    
    return weight / (height ** 2)

5. 实际案例:Web API 错误处理

示例1:RESTful API 错误处理

import json
from http import HTTPStatus

class APIError(Exception):
    """API异常基类"""
    def __init__(self, message, status_code=HTTPStatus.BAD_REQUEST, error_code=None):
        super().__init__(message)
        self.message = message
        self.status_code = status_code
        self.error_code = error_code
    
    def to_dict(self):
        return {
            "error": {
                "code": self.error_code or self.status_code,
                "message": self.message
            }
        }

class ValidationError(APIError):
    """数据验证错误"""
    def __init__(self, message, field=None):
        super().__init__(message, HTTPStatus.BAD_REQUEST, "VALIDATION_ERROR")
        self.field = field

class AuthenticationError(APIError):
    """认证错误"""
    def __init__(self, message="认证失败"):
        super().__init__(message, HTTPStatus.UNAUTHORIZED, "AUTH_ERROR")

class ResourceNotFoundError(APIError):
    """资源未找到"""
    def __init__(self, resource_type, resource_id):
        message = f"{resource_type} '{resource_id}' 未找到"
        super().__init__(message, HTTPStatus.NOT_FOUND, "NOT_FOUND")

def handle_api_request(request_handler):
    """API请求处理装饰器"""
    def wrapper(*args, **kwargs):
        try:
            return request_handler(*args, **kwargs)
        except APIError as e:
            # 已知的业务异常
            return json.dumps(e.to_dict()), e.status_code
        except Exception as e:
            # 未知异常,记录日志但不暴露细节给用户
            logging.error(f"未处理的异常: {e}", exc_info=True)
            error = APIError(
                "服务器内部错误",
                HTTPStatus.INTERNAL_SERVER_ERROR,
                "INTERNAL_ERROR"
            )
            return json.dumps(error.to_dict()), error.status_code
    return wrapper

# 使用示例
@handle_api_request
def get_user_profile(user_id):
    if not user_id or not isinstance(user_id, int):
        raise ValidationError("用户ID必须为整数", field="user_id")
    
    user = database.get_user(user_id)  # 假设的数据库操作
    if not user:
        raise ResourceNotFoundError("用户", user_id)
    
    return {"user": user}

6. 数据库操作的最佳实践 ️

示例2:数据库事务处理

import sqlite3
from contextlib import contextmanager

class DatabaseError(Exception):
    """数据库异常"""
    pass

@contextmanager
def database_transaction(db_path):
    """数据库事务上下文管理器"""
    connection = None
    try:
        connection = sqlite3.connect(db_path)
        connection.row_factory = sqlite3.Row
        yield connection
        connection.commit()
        print("✅ 事务提交成功")
    except sqlite3.Error as e:
        if connection:
            connection.rollback()
            print(" 事务回滚")
        raise DatabaseError(f"数据库操作失败: {e}")
    finally:
        if connection:
            connection.close()
            print(" 数据库连接已关闭")

def safe_database_operation():
    """安全的数据库操作示例"""
    try:
        with database_transaction("app.db") as conn:
            # 创建表
            conn.execute('''
                CREATE TABLE IF NOT EXISTS users (
                    id INTEGER PRIMARY KEY,
                    name TEXT NOT NULL,
                    email TEXT UNIQUE NOT NULL
                )
            ''')
            
            # 插入数据
            conn.execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                ("Alice", "alice@example.com")
            )
            
            # 故意制造错误(重复email)
            conn.execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                ("Bob", "alice@example.com")  # 重复email,会违反UNIQUE约束
            )
            
    except DatabaseError as e:
        print(f"❌ 数据库错误: {e}")
    except Exception as e:
        print(f"❌ 未知错误: {e}")

# 测试
safe_database_operation()

7. 文件处理的最佳实践

示例3:健壮的文件处理

import os
from pathlib import Path

class FileProcessor:
    """文件处理器"""
    
    @staticmethod
    def safe_read_file(file_path, encoding='utf-8'):
        """安全读取文件"""
        path = Path(file_path)
        
        # 前置验证
        if not path.exists():
            raise FileNotFoundError(f"文件不存在: {file_path}")
        
        if not path.is_file():
            raise ValueError(f"路径不是文件: {file_path}")
        
        if path.stat().st_size == 0:
            raise ValueError(f"文件为空: {file_path}")
        
        try:
            with open(file_path, 'r', encoding=encoding) as file:
                return file.read()
        except UnicodeDecodeError:
            # 尝试其他编码
            try:
                with open(file_path, 'r', encoding='latin-1') as file:
                    return file.read()
            except Exception as e:
                raise ValueError(f"无法解码文件: {e}")
        except PermissionError:
            raise PermissionError(f"没有读取权限: {file_path}")
    
    @staticmethod
    def safe_write_file(file_path, content, backup=True):
        """安全写入文件"""
        path = Path(file_path)
        
        # 如果需要备份且文件已存在
        if backup and path.exists():
            backup_path = path.with_suffix('.bak')
            try:
                path.rename(backup_path)
                print(f" 已创建备份: {backup_path}")
            except Exception as e:
                raise IOError(f"创建备份失败: {e}")
        
        # 确保目录存在
        path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 使用临时文件避免写入过程中出错导致原文件损坏
            temp_path = path.with_suffix('.tmp')
            with open(temp_path, 'w', encoding='utf-8') as file:
                file.write(content)
            
            # 原子操作:重命名临时文件为目标文件
            temp_path.rename(path)
            print(f"✅ 文件写入成功: {file_path}")
            
        except Exception as e:
            # 清理临时文件
            if temp_path.exists():
                temp_path.unlink()
            raise IOError(f"文件写入失败: {e}")

# 使用示例
processor = FileProcessor()

try:
    content = processor.safe_read_file("important_data.txt")
    processed_content = content.upper()
    processor.safe_write_file("processed_data.txt", processed_content)
except (FileNotFoundError, ValueError, PermissionError, IOError) as e:
    print(f"文件处理失败: {e}")

8. 配置管理的错误处理 ⚙️

示例4:配置加载器

import yaml
import os
from typing import Dict, Any

class ConfigError(Exception):
    """配置错误"""
    pass

class ConfigLoader:
    """配置加载器"""
    
    REQUIRED_KEYS = ['database', 'api_key', 'timeout']
    
    @classmethod
    def load_config(cls, config_path: str) -> Dict[str, Any]:
        """加载并验证配置文件"""
        try:
            with open(config_path, 'r') as file:
                config = yaml.safe_load(file)
        except FileNotFoundError:
            raise ConfigError(f"配置文件不存在: {config_path}")
        except yaml.YAMLError as e:
            raise ConfigError(f"配置文件格式错误: {e}")
        except Exception as e:
            raise ConfigError(f"读取配置文件失败: {e}")
        
        # 验证必需配置项
        cls._validate_config(config)
        
        return config
    
    @classmethod
    def _validate_config(cls, config: Dict[str, Any]):
        """验证配置完整性"""
        if not config:
            raise ConfigError("配置文件为空")
        
        missing_keys = []
        for key in cls.REQUIRED_KEYS:
            if key not in config:
                missing_keys.append(key)
        
        if missing_keys:
            raise ConfigError(f"缺少必需配置项: {', '.join(missing_keys)}")
        
        # 验证具体值
        if config.get('timeout') <= 0:
            raise ConfigError("超时时间必须大于0")
        
        if not config.get('api_key') or not isinstance(config['api_key'], str):
            raise ConfigError("API密钥必须为非空字符串")

# 使用示例
try:
    config = ConfigLoader.load_config("app_config.yaml")
    print("✅ 配置加载成功")
except ConfigError as e:
    print(f"❌ 配置错误: {e}")
    # 使用默认配置或退出程序

9. 异常处理检查清单 ✅

在编写异常处理代码时,问自己这些问题:

  • 我是否捕获了具体的异常类型?
  • 我是否提供了有意义的错误信息?
  • 我是否妥善清理了资源?
  • 我是否记录了重大的异常?
  • 我是否思考了所有可能的错误情况?
  • 我的异常处理是否有助于调试?
  • 我是否避免了静默失败?
  • 我是否使用了适当的异常层次结构?

10. 最终实战项目

综合示例:天气预报应用

import requests
import logging
from datetime import datetime
from typing import Optional, Dict, Any

# 自定义异常
class WeatherAPIError(Exception):
    """天气API异常"""
    pass

class InvalidLocationError(WeatherAPIError):
    """无效位置异常"""
    pass

class WeatherService:
    """天气服务"""
    
    def __init__(self, api_key: str):
        if not api_key:
            raise ValueError("API密钥不能为空")
        self.api_key = api_key
        self.base_url = "https://api.weather.com/v1"
        
        # 配置日志
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
    
    def get_weather(self, city: str, country: str) -> Dict[str, Any]:
        """获取天气信息"""
        # 参数验证
        if not city or not isinstance(city, str):
            raise InvalidLocationError("城市名称必须为非空字符串")
        
        if not country or not isinstance(country, str):
            raise InvalidLocationError("国家名称必须为非空字符串")
        
        try:
            logging.info(f"获取天气信息: {city}, {country}")
            
            response = requests.get(
                f"{self.base_url}/weather",
                params={
                    "city": city,
                    "country": country,
                    "apikey": self.api_key
                },
                timeout=10  # 设置超时
            )
            
            # 检查HTTP状态码
            response.raise_for_status()
            
            data = response.json()
            
            # 检查API响应状态
            if data.get('status') != 'success':
                raise WeatherAPIError(f"API返回错误: {data.get('message', '未知错误')}")
            
            return data
            
        except requests.exceptions.Timeout:
            raise WeatherAPIError("请求超时,请稍后重试")
        except requests.exceptions.ConnectionError:
            raise WeatherAPIError("网络连接错误,请检查网络")
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 404:
                raise InvalidLocationError(f"找不到位置: {city}, {country}")
            elif e.response.status_code == 401:
                raise WeatherAPIError("API密钥无效")
            else:
                raise WeatherAPIError(f"HTTP错误: {e}")
        except ValueError as e:
            raise WeatherAPIError(f"解析响应数据失败: {e}")
        except Exception as e:
            logging.error(f"获取天气信息时发生未知错误: {e}")
            raise WeatherAPIError("服务暂时不可用,请稍后重试")

# 使用示例
def main():
    try:
        weather_service = WeatherService("your_api_key_here")
        weather_data = weather_service.get_weather("Beijing", "China")
        print(f"✅ 天气信息: {weather_data}")
        
    except InvalidLocationError as e:
        print(f"❌ 位置错误: {e}")
    except WeatherAPIError as e:
        print(f"❌ 天气服务错误: {e}")
    except Exception as e:
        print(f"❌ 未知错误: {e}")

if __name__ == "__main__":
    main()

总结

异常处理的最佳实践:

  1. 具体明确:捕获具体异常,提供清晰错误信息
  2. 不要静默:避免无声失败,适当记录和报告
  3. 资源管理:使用上下文管理器自动清理资源
  4. 分层处理:在不同层级适当处理异常
  5. 用户友善:给用户有意义的反馈,给开发者详细的日志
  6. 防御性编程:预见可能的问题并提前处理

记住: 优秀的异常处理让程序从”能用”变为”好用”,从”脆弱”变为”健壮”!

祝贺你完成了Python异常处理的全部学习!目前你已经具备了编写健壮、专业Python程序的能力!

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...