第三十三篇:错误处理——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 异常处理的核心知识:
- 异常体系 — 理解 BaseException 到 Exception 的层次结构
- try-except-else-finally — 四种子句的正确使用场景
- raise 与自定义异常 — 设计语义清晰的异常类及异常链式传递
- 多异常捕获 — 元组形式的分组捕获
- 实战案例 — 数据验证函数库、批量容错处理器
- 最佳实践 — 精确捕获、保留异常链、避免空 except
- 资源管理 — finally 与上下文管理器结合使用
下一篇文章将深入讲解调试技巧:print、assert、logging 与 pdb。
本文是 Python 教程系列的第三十三篇。
