返回博客

33. 错误处理:try-except-finally

33. 错误处理:try-except-finally

第三十三篇:错误处理——try-except-finally 详解

前言

在程序运行过程中,错误和异常不可避免。Python 提供了完善的异常处理机制,通过 try-except-finally 可以优雅地捕获和处理各种异常情况。本文将系统讲解 Python 的异常体系及其最佳实践。

Python 异常体系

异常层次结构

BaseException
 ├── SystemExit           # sys.exit() 触发的退出
 ├── KeyboardInterrupt    # 用户按下 Ctrl+C
 ├── GeneratorExit        # 生成器关闭时触发
 └── Exception            # 所有常规异常的基类
      ├── ArithmeticError  # 算术错误
      │    ├── ZeroDivisionError
      │    ├── OverflowError
      │    └── FloatingPointError
      ├── AssertionError   # assert 失败
      ├── AttributeError   # 属性访问失败
      ├── ImportError      # 导入模块失败
      │    └── ModuleNotFoundError
      ├── LookupError      # 查找失败
      │    ├── IndexError
      │    └── KeyError
      ├── NameError        # 变量未定义
      ├── OSError          # 操作系统错误
      │    ├── FileNotFoundError
      │    ├── PermissionError
      │    └── TimeoutError
      ├── RuntimeError     # 运行时错误
      │    └── StopIteration
      ├── SyntaxError      # 语法错误
      ├── TypeError        # 类型错误
      └── ValueError       # 值错误
           ├── UnicodeError
           └── UnicodeDecodeError

查看内置异常

import builtins

# 列出所有内置异常
exceptions = [name for name in dir(builtins) 
              if isinstance(getattr(builtins, name), type) 
              and issubclass(getattr(builtins, name), Exception)]

for exc in sorted(exceptions):
    print(exc)
# AssertionError
# AttributeError
# BaseException
# BrokenPipeError
# BufferError
# BytesWarning
# ...(更多异常)

try-except 基本用法

捕获单个异常

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError as e:
        print(f"除零错误: {e}")
        return None
    else:
        print("计算成功")
        return result

print(divide(10, 3))   # 计算成功 → 3.333...
print(divide(10, 0))   # 除零错误: division by zero → None

捕获多个异常

def safe_parse(number_str):
    try:
        number = int(number_str)
        result = 100 / number
    except ValueError:
        print(f"无效的数字格式: '{number_str}'")
    except ZeroDivisionError:
        print("不能除以零")
    except (TypeError, NameError):
        print("类型或名称错误")
    else:
        print(f"结果: {result}")
    finally:
        print("无论是否出错都会执行")

safe_parse("abc")    # 无效的数字格式 → 最终执行
safe_parse("0")      # 不能除以零 → 最终执行
safe_parse("5")      # 结果: 20.0 → 最终执行

通用捕获与 re-raise

def risky_operation():
    try:
        # 模拟一些可能出错的代码
        result = eval("1 + 2")  # eval 可能产生任何异常
    except Exception as e:
        print(f"发生未知错误: {type(e).__name__}: {e}")
        raise  # ← 重新抛出异常,让调用者处理

try:
    risky_operation()
except Exception:
    print("外层捕获到了异常")

else 子句 —— 无异常时执行

import json

data = '{"name": "小明", "age": 25}'

try:
    parsed = json.loads(data)
except json.JSONDecodeError as e:
    print(f"JSON 解析失败: {e}")
else:
    # 只有没有异常时才执行
    print(f"解析成功: {parsed}")
    print(f"姓名: {parsed['name']}")

# 也可以写成更清晰的形式
try:
    connection = socket.socket()
    connection.connect(("example.com", 80))
except OSError as e:
    print(f"连接失败: {e}")
else:
    connection.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
    response = connection.recv(4096)
    connection.close()

finally 子句 —— 无论如何都执行

资源清理的最佳实践

import time

def download_file(url):
    file_handle = None
    try:
        print(f"开始下载: {url}")
        file_handle = open("downloaded.bin", "wb")
        # 模拟下载过程
        time.sleep(0.1)
        file_handle.write(b"fake data")
    except IOError as e:
        print(f"下载失败: {e}")
        return False
    finally:
        # 无论成功还是失败,都会关闭文件
        if file_handle:
            file_handle.close()
            print("文件已关闭")
    
    return True

download_file("http://example.com/file.zip")

带返回值的 finally

def demo_finally_with_return():
    try:
        return "try 中的返回值"
    except:
        return "except 中的返回值"
    finally:
        print("finally 总是被执行!")
        # return "finally 中的返回值"  # ← 会覆盖前面的 return

result = demo_finally_with_return()
print(result)
# 输出:
# finally 总是被执行!
# try 中的返回值

raise —— 主动抛出异常

基本用法

def set_age(age):
    if not isinstance(age, int):
        raise TypeError("年龄必须是整数")
    if age < 0 or age > 150:
        raise ValueError(f"年龄必须在 0-150 之间,收到: {age}")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(f"错误: {e}")  # 错误: 年龄必须在 0-150 之间,收到: -5

try:
    set_age("二十岁")
except TypeError as e:
    print(f"错误: {e}")  # 错误: 年龄必须是整数

自定义异常类

class AppError(Exception):
    """应用层基础异常"""
    def __init__(self, message, code=None):
        super().__init__(message)
        self.code = code

class AuthenticationError(AppError):
    """认证失败"""
    def __init__(self, reason="未知原因"):
        super().__init__(f"认证失败: {reason}", code=401)

class PaymentError(AppError):
    """支付错误"""
    def __init__(self, order_id, reason):
        super().__init__(f"订单 {order_id} 支付失败: {reason}", code=402)

class InsufficientFundsError(PaymentError):
    """余额不足"""
    def __init__(self, order_id, amount, balance):
        super().__init__(order_id, f"需要 {amount},余额 {balance}")

# 使用自定义异常
try:
    raise InsufficientFundsError("ORD-001", 500, 200)
except PaymentError as e:
    print(f"支付错误 [{e.code}]: {e}")
    # 支付错误 [402]: 订单 ORD-001 支付失败: 需要 500,余额 200
except AppError as e:
    print(f"应用错误: {e}")

# 可以检查异常层级关系
exc = InsufficientFundsError("ORD-002", 100, 50)
print(isinstance(exc, PaymentError))  # True
print(isinstance(exc, AppError))      # True

异常链式传递

import traceback

def database_query(query):
    # 模拟数据库查询
    raise ConnectionError("数据库连接超时")

def process_data(user_input):
    try:
        query = f"SELECT * FROM users WHERE id = {user_input}"
        return database_query(query)
    except ConnectionError as e:
        # Python 3.x 支持 from 链式传递
        raise RuntimeError(f"数据处理失败,输入: {user_input}") from e

try:
    process_data("admin; DROP TABLE users")
except RuntimeError as e:
    print(f"主异常: {e}")
    print(f"原因: {e.__cause__}")
    traceback.print_exception(type(e), e, e.__traceback__)

常见内置异常速查

异常 触发条件 示例场景
ValueError 值不正确 int("abc")
TypeError 类型不匹配 "a" + 1
KeyError 字典键不存在 d["missing"]
IndexError 索引越界 l[10] (len=5)
AttributeError 属性不存在 obj.nonexistent
NameError 变量未定义 undefined_var
ZeroDivisionError 除以零 1 / 0
FileNotFoundError 文件不存在 open("no.txt")
PermissionError 权限不足 open("/etc/passwd", "r")
ImportError 导入失败 import nonexistent
RuntimeError 一般运行时错误 触发递归上限
StopIteration 迭代结束 next() 用完

实战:数据验证函数库

class ValidationError(Exception):
    """验证失败异常"""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

def validate_email(email):
    """邮箱验证"""
    if not email:
        raise ValidationError("email", "邮箱不能为空")
    if "@" not in email:
        raise ValidationError("email", f"无效的邮箱格式: {email}")
    if "." not in email.split("@")[1]:
        raise ValidationError("email", f"域名缺少顶级后缀: {email}")
    return True

def validate_password(password):
    """密码验证"""
    if not password:
        raise ValidationError("password", "密码不能为空")
    if len(password) < 8:
        raise ValidationError("password", "密码长度至少 8 位")
    if not any(c.isupper() for c in password):
        raise ValidationError("password", "必须包含大写字母")
    if not any(c.isdigit() for c in password):
        raise ValidationError("password", "必须包含数字")
    return True

def validate_username(username):
    """用户名验证"""
    if not username:
        raise ValidationError("username", "用户名不能为空")
    if len(username) < 3:
        raise ValidationError("username", "用户名至少 3 位")
    if len(username) > 20:
        raise ValidationError("username", "用户名最多 20 位")
    if not username.isalnum():
        raise ValidationError("username", "用户名只能包含字母和数字")
    return True

def register_user(username, email, password):
    """注册用户"""
    errors = []
    
    # 收集所有验证错误
    for validator in [validate_username, validate_email, validate_password]:
        try:
            validator({"username": username, "email": email, "password": password}[
                {"validate_username": "username", "validate_email": "email", "validate_password": "password"}[validator]
            ])
        except ValidationError as e:
            errors.append(str(e))
    
    if errors:
        error_messages = "\n".join(f"  ✗ {err}" for err in errors)
        raise ValidationError("registration", f"\n{error_messages}")
    
    return f"用户 {username} 注册成功!"

# 测试
try:
    register_user("ab", "bad-email", "weak")
except ValidationError as e:
    if e.field == "registration":
        print("注册失败:")
        print(e.message)
    else:
        print(f"{e.field}: {e.message}")

实战:批量数据处理

class BatchProcessor:
    """批量处理器,跳过失败记录继续处理"""
    
    def __init__(self, records, processor_func):
        self.records = records
        self.processor_func = processor_func
        self.results = []
        self.errors = []
    
    def process(self):
        """处理所有记录,记录错误但不停止"""
        for i, record in enumerate(self.records):
            try:
                result = self.processor_func(record)
                self.results.append((i, "success", result))
            except Exception as e:
                self.errors.append((i, record, str(e)))
                print(f"记录 {i} 处理失败: {e}")
        
        self._print_report()
        return self.results, self.errors
    
    def _print_report(self):
        """打印处理报告"""
        total = len(self.records)
        success = len(self.results)
        failed = len(self.errors)
        
        print("\n" + "=" * 50)
        print("批量处理报告")
        print("=" * 50)
        print(f"总计: {total} 条记录")
        print(f"成功: {success} 条")
        print(f"失败: {failed} 条")
        
        if self.errors:
            print("\n失败详情:")
            for idx, record, error in self.errors:
                print(f"  [{idx}] 数据: {record[:30]}... 错误: {error}")

# 模拟数据处理
def parse_record(line):
    """解析一条记录"""
    parts = line.strip().split(",")
    if len(parts) != 3:
        raise ValueError(f"字段数量不对: 期望 3,实际 {len(parts)}")
    name, score_str, grade = parts
    score = int(score_str)  # 可能转换失败
    if score < 0 or score > 100:
        raise ValueError(f"分数超出范围: {score}")
    return {"name": name, "score": score, "grade": grade.strip()}

# 测试数据
records = [
    "小明, 95, A",
    "小红, 88, B",
    "小强, abc, C",  # 无效数据
    "小刚, 72, C",
    "小丽, 102, D",  # 超出范围
    "小华, 91, A",
]

processor = BatchProcessor(records, parse_record)
results, errors = processor.process()

print(f"\n成功记录的第一个: {results[0][2]}")
print(f"错误数量: {len(errors)}")

日志记录 vs 异常处理

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def old_way():
    """❌ 只用日志,不抛出异常"""
    try:
        value = int("abc")
    except ValueError as e:
        logger.error(f"转换失败: {e}")
        # 无法通知调用者出错

def new_way():
    """✅ 记录日志 + 抛出异常"""
    try:
        value = int("abc")
    except ValueError as e:
        logger.error(f"转换失败: {e}")
        raise ValueError(f"无法将 '{'abc'}' 转换为整数") from e

try:
    new_way()
except ValueError as e:
    print(f"捕获到: {e}")
    # 捕获到: 无法将 'abc' 转换为整数

最佳实践总结

✅ 推荐做法

做法 说明
精确捕获异常 只捕获你预期会发生的异常
使用 else 分离代码 无异常时的逻辑放 else 块
始终用 finally/closing 释放资源 确保清理操作一定执行
保留原始异常信息 使用 from e 保持异常链
自定义异常继承 Exception 提供语义清晰的异常类型

❌ 避免做法

# ❌ 空捕获所有异常 — 会掩盖真正的 bug
try:
    dangerous_code()
except:
    pass

# ✅ 正确做法 — 即使忽略也要记录
try:
    dangerous_code()
except Exception:
    import traceback
    traceback.print_exc()

# ❌ 过宽的捕获 — 连 KeyboardInterrupt 都会被吞掉
try:
    long_running_task()
except Exception:
    print("出错了")

# ✅ 正确做法 — 排除用户中断
try:
    long_running_task()
except (ValueError, TypeError, OSError):
    print("处理失败")

# ❌ 在 except 中 return — 跳过 finally
try:
    return value
except:
    return None  # finally 还会执行!

# ✅ 如果必须 return,使用局部变量
result = None
try:
    result = compute()
except:
    result = fallback_value
return result  # ← finally 之后才执行

总结

本文系统讲解了 Python 异常处理的核心知识:

  1. 异常体系 — 理解 BaseException 到 Exception 的层次结构
  2. try-except-else-finally — 四种子句的正确使用场景
  3. raise 与自定义异常 — 设计语义清晰的异常类及异常链式传递
  4. 多异常捕获 — 元组形式的分组捕获
  5. 实战案例 — 数据验证函数库、批量容错处理器
  6. 最佳实践 — 精确捕获、保留异常链、避免空 except
  7. 资源管理 — finally 与上下文管理器结合使用

下一篇文章将深入讲解调试技巧:print、assert、logging 与 pdb。


本文是 Python 教程系列的第三十三篇。