Week 05: 异常处理与上下文管理
掌握 Python 异常机制、自定义异常、上下文管理器与 contextlib 模块.
1. 异常基础
1.1 什么是异常
异常是程序运行时发生的错误, 会中断正常的程序流程.
# ZeroDivisionError
result = 10 / 0
# KeyError
d = {}
d["key"]
# IndexError
lst = [1, 2, 3]
lst[10]
# TypeError
"hello" + 5
# ValueError
int("abc")1.2 try/except 基本语法
try:
result = 10 / 0
except ZeroDivisionError:
print("除数不能为零")1.3 捕获多个异常
try:
result = int(input("Enter a number: "))
except ValueError:
print("输入不是有效数字")
except KeyboardInterrupt:
print("用户中断")
# 或者一起捕获
try:
result = int(input("Enter a number: "))
except (ValueError, TypeError):
print("输入无效")1.4 获取异常信息
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"错误类型: {type(e).__name__}")
print(f"错误信息: {e}")1.5 else 与 finally
try:
f = open("file.txt")
except FileNotFoundError:
print("文件不存在")
else:
# try 成功时执行
content = f.read()
f.close()
finally:
# 无论成功与否都执行
print("清理操作")finally 的保证:
def example():
try:
return "try"
finally:
print("finally 仍然执行")
result = example()
# finally 仍然执行
print(result) # try2. 异常层次结构
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── FloatingPointError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── ValueError
├── TypeError
├── AttributeError
└── ...2.1 Exception vs BaseException
- 捕获
Exception: 捕获大多数异常 - 不要捕获
BaseException: 会阻止KeyboardInterrupt和SystemExit
# 推荐
try:
risky_operation()
except Exception as e:
handle_error(e)
# 不推荐
try:
risky_operation()
except: # 捕获所有, 包括 KeyboardInterrupt
pass3. 抛出异常
3.1 raise 语句
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
try:
divide(10, 0)
except ValueError as e:
print(e)3.2 重新抛出异常
try:
result = 10 / 0
except ZeroDivisionError:
print("记录日志")
raise # 重新抛出原异常3.3 异常链 (Exception Chaining)
try:
result = 10 / 0
except ZeroDivisionError as e:
raise RuntimeError("计算失败") from e
# RuntimeError: 计算失败
#
# The above exception was the direct cause of the following exception:
#
# ZeroDivisionError: division by zero4. 自定义异常
class ValidationError(Exception):
"""验证错误基类"""
pass
class InvalidEmailError(ValidationError):
"""邮箱格式错误"""
def __init__(self, email, message="无效的邮箱地址"):
self.email = email
self.message = message
super().__init__(f"{message}: {email}")
def validate_email(email):
if "@" not in email:
raise InvalidEmailError(email)
return True
try:
validate_email("invalid-email")
except InvalidEmailError as e:
print(f"错误: {e}")
print(f"邮箱: {e.email}")4.1 warnings 模块
用于发出非致命警告, API 弃用通知:
import warnings
# 发出警告
def deprecated_function():
warnings.warn(
"deprecated_function 已弃用, 请使用 new_function",
DeprecationWarning,
stacklevel=2 # 指向调用者
)
# 警告类型
warnings.warn("一般警告", UserWarning)
warnings.warn("弃用警告", DeprecationWarning)
warnings.warn("即将移除", PendingDeprecationWarning)
warnings.warn("运行时警告", RuntimeWarning)
# 控制警告行为
warnings.filterwarnings("ignore", category=DeprecationWarning) # 忽略
warnings.filterwarnings("error", category=UserWarning) # 转为异常
warnings.filterwarnings("always") # 始终显示
# 临时修改警告行为
with warnings.catch_warnings():
warnings.simplefilter("ignore")
deprecated_function() # 警告被忽略命令行控制:
# 忽略所有弃用警告
python -W ignore::DeprecationWarning script.py
# 将警告转为错误
python -W error script.py5. 上下文管理器
5.1 with 语句
# 传统方式
f = open("file.txt")
try:
content = f.read()
finally:
f.close()
# 使用 with
with open("file.txt") as f:
content = f.read()
# 自动关闭文件5.2 上下文管理协议
上下文管理器需要实现 __enter__ 和 __exit__ 方法:
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print("Opening file")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing file")
if self.file:
self.file.close()
# 返回 True 则抑制异常
return False
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")5.3 __exit__ 参数
class SuppressError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print(f"抑制 ValueError: {exc_val}")
return True # 抑制异常
return False # 传播异常
with SuppressError():
raise ValueError("测试错误")
print("这行不会执行")
print("继续执行") # 会执行5.4 多个上下文管理器
with open("input.txt") as infile, open("output.txt", "w") as outfile:
content = infile.read()
outfile.write(content.upper())6. contextlib 模块
6.1 @contextmanager 装饰器
使用生成器简化上下文管理器:
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield # 执行 with 块
end = time.time()
print(f"耗时: {end - start:.4f}s")
with timer():
import time
time.sleep(1)
# 耗时: 1.0012s带返回值:
@contextmanager
def open_file(filename, mode):
f = open(filename, mode)
try:
yield f
finally:
f.close()
with open_file("test.txt", "w") as f:
f.write("Hello")6.2 suppress 抑制异常
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("nonexistent.txt")
# 不会抛出异常
# 等价于
try:
os.remove("nonexistent.txt")
except FileNotFoundError:
pass6.3 redirect_stdout
from contextlib import redirect_stdout
from io import StringIO
buffer = StringIO()
with redirect_stdout(buffer):
print("Hello, World!")
output = buffer.getvalue()
print(f"Captured: {output}") # Captured: Hello, World!6.4 ExitStack
动态管理多个上下文管理器:
from contextlib import ExitStack
filenames = ["file1.txt", "file2.txt", "file3.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(fn)) for fn in filenames]
# 所有文件都会在退出时关闭7. 异常处理模式
7.1 EAFP vs LBYL
LBYL (Look Before You Leap): 先检查再操作
# LBYL
if "key" in d:
value = d["key"]
else:
value = defaultEAFP (Easier to Ask Forgiveness than Permission): 先操作再处理异常
# EAFP (Python 推荐)
try:
value = d["key"]
except KeyError:
value = default7.2 日志记录
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
try:
result = risky_operation()
except Exception as e:
logger.exception("操作失败") # 包含堆栈信息
raise7.3 清理资源
class Connection:
def __init__(self):
self.connected = True
print("Connected")
def close(self):
self.connected = False
print("Disconnected")
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# 确保连接被关闭
with Connection() as conn:
# 使用连接
pass8. 调试技巧
8.1 traceback 模块
import traceback
try:
1 / 0
except ZeroDivisionError:
# 获取格式化的堆栈信息
tb = traceback.format_exc()
print(tb)8.2 pdb 调试器
import pdb
def buggy_function():
x = 10
pdb.set_trace() # 断点
y = 0
return x / y
buggy_function()pdb 常用命令:
n(next): 下一行s(step): 进入函数c(continue): 继续执行p <expr>: 打印表达式q(quit): 退出
8.3 breakpoint() (Python 3.7+)
def buggy_function():
x = 10
breakpoint() # 自动调用 pdb.set_trace()
y = 0
return x / y9. 练习
9.1 安全除法函数
实现一个安全的除法函数, 处理除零和类型错误.
9.2 数据库连接管理器
实现一个数据库连接的上下文管理器, 自动提交或回滚事务.
9.3 重试装饰器
实现一个装饰器, 当函数抛出异常时自动重试 N 次.
10. 思考题
- 为什么不建议捕获所有异常 (
except:)? finally一定会执行吗?- 什么时候应该抑制异常?
- EAFP 和 LBYL 各自的适用场景?
- 为什么
with语句比try/finally更好?
11. 本周小结
- 异常处理: try/except/else/finally.
- 异常层次: Exception 继承树.
- 自定义异常: 继承 Exception.
- 上下文管理器:
__enter__,__exit__, with 语句. - contextlib: @contextmanager, suppress, ExitStack.
- 处理模式: EAFP vs LBYL.
- 调试: traceback, pdb, breakpoint.
好的错误处理是健壮程序的基础. 上下文管理器让资源管理变得优雅而安全.