第九章:异常处理
本章学习目标
- 理解 Python 异常机制
- 掌握 try-except 语法和高级用法
- 学会自定义异常
- 理解异常链和 traceback
- 掌握上下文管理器与异常处理
- 学会使用 logging 模块记录错误
9.1 异常基础
9.1.1 什么是异常
异常是程序执行过程中发生的错误,会中断正常的程序流程。与语法错误不同,异常发生在程序运行期间。
# 这些都会引发异常
result = 10 / 0 # ZeroDivisionError: division by zero
print(undefined_var) # NameError: name 'undefined_var' is not defined
"123" + 456 # TypeError: can only concatenate str (not "int") to str
open("not_exist.txt") # FileNotFoundError: [Errno 2] No such file or directory
lst = [1, 2, 3]
print(lst[10]) # IndexError: list index out of range
d = {"a": 1}
print(d["b"]) # KeyError: 'b'
9.1.2 异常层级结构
Python 的异常有一个层级结构:
BaseException
├── SystemExit # sys.exit() 调用
├── KeyboardInterrupt # Ctrl+C 中断
├── GeneratorExit # 生成器关闭
└── Exception # 所有内置异常的基类
├── StopIteration
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError (IOError)
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ...
├── TypeError
├── ValueError
├── RuntimeError
└── ...
9.1.3 异常 vs 错误
- 异常:程序逻辑问题,可以被捕获和处理
- 错误:通常是系统级别的问题,如内存溢出
9.2 try-except 语句
9.2.1 基本语法
# 最简单的形式
try:
result = 10 / 0
except ZeroDivisionError:
print("不能除以零!")
# 带有异常对象
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"发生错误: {e}")
print(f"异常类型: {type(e).__name__}")
9.2.2 多个 except
# 分别处理不同异常
try:
value = int("abc") # ValueError
lst = [1, 2, 3]
print(lst[10]) # IndexError
except ValueError:
print("值转换错误")
except IndexError:
print("索引错误")
# except 顺序:先具体后一般
try:
result = 10 / 0
except ZeroDivisionError:
print("除零错误")
except ArithmeticError:
print("算术错误")
except Exception:
print("其他错误")
9.2.3 捕获所有异常
# 捕获所有 Exception
try:
# 可能发生任何异常的代码
pass
except Exception as e:
print(f"发生错误: {e}")
# 捕获所有异常(包括非 Exception 子类)
try:
pass
except BaseException as e:
print(f"异常: {e}")
9.2.4 捕获多个异常(元组形式)
# 在一个 except 中处理多个异常
try:
# 可能引发多种异常的代码
pass
except (ValueError, TypeError, KeyError) as e:
print(f"发生错误: {e}")
9.2.5 else 子句
else 子句在没有异常发生时执行:
try:
result = 10 / 2
except ZeroDivisionError:
print("发生除零错误")
else:
print(f"计算成功,结果是 {result}")
# 只有没有异常时才会执行
9.2.6 finally 子句
finally 子句无论是否有异常都会执行:
# 基本用法
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("文件不存在")
finally:
# 始终执行
if 'file' in locals():
file.close()
# 典型用途:资源清理
try:
result = 10 / 2
except ZeroDivisionError:
print("错误")
finally:
print("程序结束")
9.2.7 完整结构
try:
# 可能发生异常的代码
result = 10 / 2
except ZeroDivisionError:
# 处理除零错误
print("不能除以零")
except ValueError:
# 处理值错误
print("值错误")
except Exception as e:
# 处理其他所有异常
print(f"未知错误: {e}")
else:
# 没有异常时执行
print(f"结果是 {result}")
finally:
# 始终执行
print("清理工作")
9.3 抛出异常
9.3.1 raise 语句
使用 raise 语句抛出异常:
# 基本形式
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("除数不能为零")
return a / b
try:
result = divide(10, 0)
except ValueError as e:
print(e) # 除数不能为零
9.3.2 重新抛出异常
在 except 块中重新抛出异常:
def handle_error() -> None:
try:
# 可能异常的代码
pass
except Exception as e:
print(f"记录错误: {e}")
raise # 重新抛出原始异常
9.3.3 raise from
Python 3 支持异常链:
# 显式异常链
try:
int("abc")
except ValueError as e:
raise RuntimeError("处理失败") from e
# 隐式异常链
try:
int("abc")
except ValueError:
# 在 except 块中抛出的新异常会自动链接
raise RuntimeError("处理失败")
9.3.4 断言 raise
# 使用 assert 抛出 AssertionError
def divide(a: float, b: float) -> float:
assert b != 0, "除数不能为零"
return a / b
9.4 自定义异常
9.4.1 定义异常类
# 简单的自定义异常
class ValidationError(Exception):
"""验证错误异常"""
pass
# 带有属性的异常
class NegativeValueError(ValueError):
"""负数值错误"""
def __init__(self, value: float, message: str | None = None) -> None:
self.value = value
self.message = message or f"值不能为负数: {value}"
super().__init__(self.message)
9.4.2 使用自定义异常
def set_age(age: int) -> int:
if age < 0:
raise NegativeValueError(age, "年龄不能为负数")
if age > 150:
raise ValidationError("年龄超出合理范围")
return age
try:
set_age(-5)
except NegativeValueError as e:
print(f"错误: {e.message}")
print(f"无效值: {e.value}")
except ValidationError as e:
print(f"验证错误: {e}")
9.4.3 异常层次结构
class AppError(Exception):
"""应用基础异常"""
pass
class DataError(AppError):
"""数据相关错误"""
pass
class ValidationError(DataError):
"""验证错误"""
pass
class StorageError(AppError):
"""存储相关错误"""
pass
# 使用
try:
raise ValidationError("数据无效")
except AppError as e:
print(f"应用错误: {e}") # 会被捕获
except ValidationError as e:
print(f"验证错误: {e}")
9.4.4 实战:验证器示例
class ValidationError(Exception):
"""验证错误"""
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class Validator:
"""数据验证器"""
@staticmethod
def validate_email(email: str) -> None:
if "@" not in email:
raise ValidationError("email", "邮箱格式无效")
@staticmethod
def validate_age(age: int) -> None:
if age < 0:
raise ValidationError("age", "年龄不能为负数")
if age > 150:
raise ValidationError("age", "年龄超出合理范围")
@staticmethod
def validate_user(data: dict) -> None:
errors: list[str] = []
try:
Validator.validate_email(data.get("email", ""))
except ValidationError as e:
errors.append(str(e))
try:
Validator.validate_age(data.get("age", 0))
except ValidationError as e:
errors.append(str(e))
if errors:
raise ValidationError("user", "; ".join(errors))
# 使用
user_data = {"email": "invalid", "age": -5}
try:
Validator.validate_user(user_data)
except ValidationError as e:
print(f"验证失败: {e.field}")
print(e.message)
9.5 traceback 模块
9.5.1 打印异常信息
import traceback
try:
1 / 0
except Exception:
# 打印完整的 traceback
traceback.print_exc()
# 获取为字符串
error_str = traceback.format_exc()
print(error_str)
9.5.2 记录 traceback
import logging
import traceback
logging.basicConfig(level=logging.ERROR)
try:
1 / 0
except Exception:
logging.error("发生错误:\n%s", traceback.format_exc())
9.5.3 手动控制 traceback
import traceback
try:
# 可能异常的代码
pass
except Exception:
# 保存 traceback
exc_type, exc_value, exc_tb = sys.exc_info()
# 格式化
tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
print(tb_str)
9.6 上下文管理器与异常
9.6.1 contextmanager
from contextlib import contextmanager
@contextmanager
def timer(name: str):
"""计时上下文管理器"""
import time
start = time.time()
try:
yield # 执行 with 块中的代码
finally:
end = time.time()
print(f"{name} 耗时: {end - start:.2f}秒")
# 使用
with timer("计算"):
result = sum(range(1000000))
9.6.2 suppress
from contextlib import suppress
import os
# 忽略特定异常
with suppress(FileNotFoundError):
os.remove("not_exist.txt")
# 等价于
try:
os.remove("not_exist.txt")
except FileNotFoundError:
pass
9.6.3 ExitStack
from contextlib import ExitStack
# 管理多个上下文管理器
with ExitStack() as stack:
files = [stack.enter_context(open(f"file{i}.txt", "r")) for i in range(3)]
# 所有文件会在离开 with 块时自动关闭
9.6.4 redirect_stderr / redirect_stdout
from contextlib import redirect_stderr
import sys
# 重定向 stderr
with open("error.log", "w") as f:
with redirect_stderr(f):
print("这条消息会写入文件", file=sys.stderr)
9.7 logging 模块
9.7.1 基础配置
import logging
# 基础配置
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# 获取 logger
logger = logging.getLogger(__name__)
# 使用
logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误")
9.7.2 日志级别
| 级别 | 数值 | 说明 |
|---|---|---|
| CRITICAL | 50 | 严重错误 |
| ERROR | 40 | 错误 |
| WARNING | 30 | 警告 |
| INFO | 20 | 信息 |
| DEBUG | 10 | 调试 |
| NOTSET | 0 | 不设置 |
9.7.3 记录异常
import logging
logger = logging.getLogger(__name__)
try:
1 / 0
except Exception:
# 使用 exc_info=True 记录异常信息
logger.exception("发生错误")
# 等价于
# logger.error("发生错误", exc_info=True)
9.7.4 日志配置
import logging
# 创建 logger
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)
# 创建处理器
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler("app.log", encoding="utf-8")
console_handler.setLevel(logging.INFO)
file_handler.setLevel(logging.DEBUG)
# 创建格式化器
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# 绑定
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 使用
logger.info("信息")
logger.error("错误")
9.7.5 实战:完整的异常处理
import logging
import sys
from pathlib import Path
class ErrorHandler:
"""错误处理器"""
def __init__(self, log_file: str = "error.log") -> None:
self.log_file = Path(log_file)
self._setup_logging()
def _setup_logging(self) -> None:
"""配置日志"""
self.logger = logging.getLogger("app")
self.logger.setLevel(logging.DEBUG)
# 文件处理器
file_handler = logging.FileHandler(
self.log_file, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 格式化
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)
def handle_exception(self, exc_type, exc_value, exc_tb) -> None:
"""处理未捕获的异常"""
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_tb)
return
self.logger.critical(
"未捕获的异常",
exc_info=(exc_type, exc_value, exc_tb)
)
def install(self) -> None:
"""安装全局异常处理器"""
sys.excepthook = self.handle_exception
# 使用
def main():
handler = ErrorHandler("app.log")
# 模拟错误
try:
result = 10 / 0
except Exception:
handler.logger.exception("计算错误")
handler.install()
if __name__ == "__main__":
main()
9.8 异常最佳实践
9.8.1 原则
- 捕获具体异常:不要使用
except:
# ✗ 不推荐
try:
do_something()
except:
pass
# ✓ 推荐
try:
do_something()
except ValueError as e:
print(f"值错误: {e}")
except TypeError as e:
print(f"类型错误: {e}")
- 保留异常信息:使用
from e保留原始异常
# ✗ 不推荐
try:
int("abc")
except ValueError:
raise RuntimeError("处理失败")
# ✓ 推荐
try:
int("abc")
except ValueError as e:
raise RuntimeError("处理失败") from e
- 不要忽略异常:至少记录日志
# ✗ 不推荐
try:
do_something()
except Exception:
pass # 静默忽略
# ✓ 推荐
try:
do_something()
except Exception as e:
logging.warning(f"跳过错误: {e}")
- 使用 finally 清理资源
# 文件操作
file = None
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("文件不存在")
finally:
if file:
file.close()
# 推荐使用 with 语句
9.8.2 异常处理示例
import logging
def read_number(filename: str) -> int | None:
"""读取文件中的数字"""
logger = logging.getLogger(__name__)
try:
with open(filename, "r", encoding="utf-8") as f:
content = f.read().strip()
return int(content)
except FileNotFoundError:
logger.error(f"文件不存在: {filename}")
except PermissionError:
logger.error(f"没有权限读取: {filename}")
except ValueError:
logger.error(f"文件内容不是有效数字")
except Exception as e:
logger.exception(f"未知错误: {e}")
return None
综合示例
示例 1:安全的除法函数
def safe_divide(a: float, b: float) -> float | None:
"""安全的除法运算"""
try:
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
except ZeroDivisionError as e:
print(f"错误: {e}")
return None
def divide(a: float, b: float) -> float:
"""除法运算,失败时抛出异常"""
if b == 0:
raise ZeroDivisionError(f"不能除以零: {a} / {b}")
return a / b
# 测试
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # None
print(divide(10, 2)) # 5.0
示例 2:配置加载器
import json
from pathlib import Path
class ConfigLoadError(Exception):
"""配置加载错误"""
pass
def load_config(filepath: str) -> dict:
"""加载配置文件"""
path = Path(filepath)
if not path.exists():
raise ConfigLoadError(f"配置文件不存在: {filepath}")
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
raise ConfigLoadError(f"配置文件格式错误: {e}")
except PermissionError:
raise ConfigLoadError(f"没有权限读取配置文件")
# 测试
try:
config = load_config("config.json")
except ConfigLoadError as e:
print(f"加载配置失败: {e}")
示例 3:重试机制
import time
from functools import wraps
def retry(max_attempts: int = 3, delay: float = 1.0):
"""重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt < max_attempts:
print(f"第 {attempt} 次尝试失败,{delay}秒后重试...")
time.sleep(delay)
else:
print(f"重试 {max_attempts} 次后仍然失败")
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=1.0)
def unstable_function():
"""模拟不稳定的函数"""
import random
if random.random() > 0.7:
return "成功!"
raise RuntimeError("随机失败")
# 测试
for _ in range(5):
try:
result = unstable_function()
print(f"结果: {result}")
except RuntimeError as e:
print(f"最终失败: {e}")
课后练习
练习 9.1:基本异常处理
实现一个安全的除法函数,处理除数为零的情况。
练习 9.2:自定义异常
创建一个 InvalidEmailError 异常类,用于验证邮箱格式。
练习 9.3:文件读取
实现一个安全的文件读取函数,处理文件不存在、权限错误等情况。
练习 9.4:上下文管理器
实现一个计时上下文管理器,测量代码执行时间。
练习 9.5:验证器
创建一个完整的输入验证函数,支持:
- 验证邮箱格式
- 验证手机号
- 验证密码强度
- 收集所有错误
练习 9.6:重试装饰器
实现一个重试装饰器,支持最大重试次数和延迟。
练习 9.7:日志系统
创建一个完整的日志系统,支持:
- 控制台输出
- 文件输出
- 日志轮转
- 异常记录
本章小结
本章我们详细学习了 Python 的异常处理:
异常基础:
- 异常的概念和层级结构
- 常见异常类型
try-except:
- 基本语法
- 多个 except
- else 和 finally
- 捕获所有异常
抛出异常:
- raise 语句
- 异常链
自定义异常:
- 定义异常类
- 异常层次结构
traceback:
- 打印和记录异常信息
上下文管理器:
- 与异常处理结合
logging:
- 日志配置和使用
掌握异常处理是编写健壮程序的关键技能。