Python(三十七) 上下文管理器完整教程
目录
- 开篇:从"借书"理解上下文管理器
- with 语句的核心原理
- 基于类的上下文管理器:enter 与 exit
- exit 的三个异常参数详解
- contextlib 标准库工具
- 综合实战案例
- 常见误区与面试题
一、开篇:从"借书"理解上下文管理器
1.1 生活化类比
你去图书馆借书看,完整的流程是:
① 走进图书馆(获取资源)
② 找书、看书(使用资源)
③ 把书放回书架,离开(释放资源)
无论书好不好看、无论你中途接不接电话,第三步一定要做——书必须还回去,否则其他人借不到。
编程中也有类似的"借还"场景:
| 场景 | 获取资源 | 释放资源 |
|---|---|---|
| 操作文件 | open() |
close() |
| 操作数据库 | connect() |
connection.close() |
| 获取锁 | lock.acquire() |
lock.release() |
| 网络连接 | socket.connect() |
socket.close() |
**上下文管理器(Context Manager)**就是帮你自动"归还"资源的机制。with 语句就是使用这个机制的语法。
1.2 没有 with 的世界(反面教材)
# 场景:读取文件内容
# === 错误写法1:忘了关闭文件 ===
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
# 忘了 f.close()!文件句柄泄漏,可能导致其他程序无法访问该文件
# === 错误写法2:中途出错,close 不会执行 ===
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
# 假设这里执行了某些可能出错的代码...
# 如果出错了,程序跳到 except,f.close() 被跳过
f.close()
# === 正确但啰嗦的写法3:try/finally ===
f = None
try:
f = open("data.txt", "r", encoding="utf-8")
content = f.read()
# 假设这里可能出错
finally:
if f is not None:
f.close() # 无论如何都会执行,但写法很繁琐
# 每次都要写 try/finally,太啰嗦了!
1.3 有 with 的世界(优雅解法)
# 一行 with 搞定上面所有操作!
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# 即使这里出错,文件也会自动关闭
# 出了 with 块,f 自动关闭,无需手动操作
with 做的事情:你只要告诉它"借什么",它自动帮你"还"。无论 with 块里的代码正常结束还是中途出错,资源都会被正确释放。
1.4 本教程学习路线
with 语句的原理(底层调了什么)
│
├── 基于类实现上下文管理器(__enter__ + __exit__)
│
├── __exit__ 异常处理机制(三个参数 + 返回值控制)
│
├── contextlib 工具库
│ ├── @contextmanager(用生成器快速实现)
│ ├── closing()(把有 close 的对象变成上下文管理器)
│ ├── suppress()(忽略指定异常)
│ └── ContextDecorator(既是装饰器又是上下文管理器)
│
└── 综合实战案例(计时器、临时清理、异常日志)
二、with 语句的核心原理
2.1 with 语句的执行流程
with open("data.txt", "r") as f:
content = f.read()
Python 解释器在背后做了这些事:
步骤1:open("data.txt", "r") → 创建一个文件对象
步骤2:调用 文件对象.__enter__() → 返回文件对象自身,赋值给 f
步骤3:执行 with 块内的代码 → content = f.read()
步骤4:无论步骤3是否出错,调用 文件对象.__exit__(exc_type, exc_val, traceback)
- 如果步骤3没出错:传入 (None, None, None)
- 如果步骤3出错了:传入异常的具体信息
2.2 等价代码(彻底理解 with 的底层)
# 这句代码:
# with 表达式 as 变量:
# 代码块
# 完全等价于:
管理器 = 表达式 # 例如:管理器 = open("data.txt")
变量 = 管理器.__enter__() # 例如:f = 文件对象.__enter__()
try:
代码块 # 执行 with 块里的代码
except:
# 如果出错,让 __exit__ 决定怎么处理
if not 管理器.__exit__(异常类型, 异常值, traceback):
raise # __exit__ 返回 False → 重新抛出异常
else:
# 如果没出错
管理器.__exit__(None, None, None)
2.3 手动模拟 with 的底层过程
class MyOpen:
"""模拟文件打开的上下文管理器——帮助理解 with 原理"""
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""进入上下文:打开文件"""
print(f" [__enter__] 打开文件:{self.filename}")
self.file = open(self.filename, self.mode)
return self.file # 这个返回值赋给 as 后面的变量
def __exit__(self, exc_type, exc_val, traceback):
"""退出上下文:关闭文件"""
print(f" [__exit__] 关闭文件:{self.filename}")
if self.file:
self.file.close()
# 返回 False(或不返回)→ 异常继续向上抛
# === 演示:正常结束 ===
print("=== 正常情况 ===")
with MyOpen("test.txt", "w") as f:
print(" [with块] 写入数据...")
f.write("Hello")
# 输出:
# === 正常情况 ===
# [__enter__] 打开文件:test.txt
# [with块] 写入数据...
# [__exit__] 关闭文件:test.txt
# === 演示:中途出错 ===
print("\n=== 出错情况 ===")
try:
with MyOpen("test.txt", "r") as f:
print(" [with块] 读取中...")
raise ValueError("模拟一个错误!") # 故意抛出异常
except ValueError:
print(" [外部] 捕获到了异常,但文件已经关闭了!")
# 输出:
# === 出错情况 ===
# [__enter__] 打开文件:test.txt
# [with块] 读取中...
# [__exit__] 关闭文件:test.txt ← 即使出错,文件也关闭了!
# [外部] 捕获到了异常,但文件已经关闭了!
关键发现:即使在 with 块中抛出异常,__exit__ 仍然会被调用,资源仍然被正确释放。
2.4 with 语句的多种写法
# 写法1:单资源
with open("a.txt") as f:
pass
# 写法2:多资源(逗号分隔)
with open("a.txt") as f1, open("b.txt") as f2:
pass
# 写法3:不获取资源(纯控制)——某些上下文管理器 as 后面可以没有变量
# 例如:with lock: 或 with redirect_stdout(f):
# 写法4:多资源嵌套(Python 3.10+ 可用括号换行)
with (
open("input.txt") as f_in,
open("output.txt", "w") as f_out,
):
f_out.write(f_in.read())
2.5 with 能管理的对象
# 以下都是 Python 内置的上下文管理器,可以直接用 with
# ① 文件对象
with open("file.txt") as f:
pass
# ② 线程锁
import threading
lock = threading.Lock()
with lock: # 自动 acquire 和 release
# 临界区代码
pass
# ③ 数据库连接
import sqlite3
with sqlite3.connect("test.db") as conn: # 自动 commit/rollback 和 close
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
# ④ 临时目录
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
# tmpdir 是临时目录的路径
# 退出 with 后自动删除整个目录
pass
# ⑤ 网络连接(如 urllib)
from urllib.request import urlopen
with urlopen("https://www.python.org") as response:
html = response.read()
新手易踩坑:
- 坑 1:with 后面的表达式不能是任意对象 → 必须实现
__enter__和__exit__方法- 坑 2:
as f只能获取__enter__的返回值,不是 with 后面的表达式本身- 坑 3:出了
with块后,资源已释放,不要再使用f(如f.read())
课堂小练习 1
用 with 语句实现:打开一个文件写入 "Hello World",然后再次用 with 读出来并打印。对比用 try/finally 的写法,体会 with 的简洁。
点击查看参考答案
# with 写法
with open("hello.txt", "w", encoding="utf-8") as f:
f.write("Hello World")
with open("hello.txt", "r", encoding="utf-8") as f:
print(f.read()) # Hello World
# 对比 try/finally 写法(同样功能但更啰嗦)
f = None
try:
f = open("hello.txt", "w", encoding="utf-8")
f.write("Hello World")
finally:
if f:
f.close()
f = None
try:
f = open("hello.txt", "r", encoding="utf-8")
print(f.read())
finally:
if f:
f.close()
三、基于类的上下文管理器:enter 与 exit
3.1 协议定义
要实现一个上下文管理器,类必须定义两个方法:
| 方法 | 调用时机 | 参数 | 返回值 |
|---|---|---|---|
__enter__(self) |
进入 with 块时 |
只有 self |
赋给 as 后面的变量 |
__exit__(self, exc_type, exc_val, exc_tb) |
退出 with 块时 |
异常信息(三个参数) | True 抑制异常,False 继续抛出 |
┌────────────────────────────────────────────┐
│ with MyManager() as obj: │
│ # 1. MyManager() 创建实例 │
│ # 2. 调用实例.__enter__() → 返回 obj │
│ # 3. 执行 with 块内的代码 │
│ # 4. 调用实例.__exit__(...) │
│ # └─ 没出错 → (None, None, None) │
│ # └─ 出错了 → (异常类型, 异常值, │
│ # traceback) │
└────────────────────────────────────────────┘
3.2 最简实现:文件操作的上下文管理器
class ManagedFile:
"""文件操作的上下文管理器 —— 自动关闭文件"""
def __init__(self, filename, mode="r", encoding="utf-8"):
"""初始化:记录文件名和模式,但不打开文件"""
self.filename = filename
self.mode = mode
self.encoding = encoding
self.file = None # 文件句柄,初始为 None
def __enter__(self):
"""进入上下文:打开文件,返回文件对象"""
print(f"[打开] {self.filename}")
self.file = open(self.filename, self.mode, encoding=self.encoding)
return self.file # 这个值赋给 as 后面的变量
def __exit__(self, exc_type, exc_val, exc_tb):
"""退出上下文:关闭文件(无论是否发生异常)"""
if self.file:
self.file.close()
print(f"[关闭] {self.filename}")
# 返回 False(或不返回):异常继续向上传播
# 返回 True:吞掉异常,外部不会感知到
return False
# === 使用 ===
with ManagedFile("greeting.txt", "w") as f:
f.write("你好,上下文管理器!\n")
f.write("第二行内容\n")
# 输出:
# [打开] greeting.txt
# [关闭] greeting.txt
# 验证写入的内容
with ManagedFile("greeting.txt", "r") as f:
content = f.read()
print(content)
# 输出:
# [打开] greeting.txt
# [关闭] greeting.txt
# 你好,上下文管理器!
# 第二行内容
3.3 复杂实现:数据库连接的上下文管理器
import sqlite3
class DatabaseConnection:
"""数据库连接的上下文管理器 —— 自动 commit/rollback 和关闭"""
def __init__(self, db_path):
"""初始化:记录数据库路径"""
self.db_path = db_path
self.connection = None
self.cursor = None
def __enter__(self):
"""进入上下文:建立连接,返回游标"""
print(f"[数据库] 连接 {self.db_path}")
self.connection = sqlite3.connect(self.db_path)
self.cursor = self.connection.cursor()
# 创建示例表(如果不存在)
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score REAL
)
""")
return self.cursor # 返回游标,方便调用者直接执行 SQL
def __exit__(self, exc_type, exc_val, exc_tb):
"""退出上下文:根据是否有异常决定 commit 还是 rollback"""
if exc_type is None:
# 没有异常 → 提交事务
self.connection.commit()
print(f"[数据库] 提交事务 ✓")
else:
# 有异常 → 回滚事务
self.connection.rollback()
print(f"[数据库] 回滚事务 ✗ (原因: {exc_type.__name__}: {exc_val})")
# 无论如何都要关闭连接
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
print(f"[数据库] 连接已关闭")
return False # 异常继续向上传播
# === 使用:正常情况 ===
print("=== 正常操作 ===")
with DatabaseConnection("school.db") as db:
db.execute("INSERT INTO students (name, score) VALUES (?, ?)", ("小明", 95))
db.execute("INSERT INTO students (name, score) VALUES (?, ?)", ("小红", 88))
# 输出:
# === 正常操作 ===
# [数据库] 连接 school.db
# [数据库] 提交事务 ✓
# [数据库] 连接已关闭
# === 使用:异常情况 ===
print("\n=== 异常操作 ===")
try:
with DatabaseConnection("school.db") as db:
db.execute("INSERT INTO students (name, score) VALUES (?, ?)", ("小刚", 75))
raise ValueError("模拟数据库写入后发生的错误!")
except ValueError as e:
print(f"外部捕获到异常:{e}")
# 输出:
# === 异常操作 ===
# [数据库] 连接 school.db
# [数据库] 回滚事务 ✗ (原因: ValueError: 模拟数据库写入后发生的错误!)
# [数据库] 连接已关闭
# 外部捕获到异常:模拟数据库写入后发生的错误!
# === 验证数据 ===
print("\n=== 验证数据 ===")
with DatabaseConnection("school.db") as db:
result = db.execute("SELECT * FROM students").fetchall()
for row in result:
print(f" {row}")
# 只有小明和小红(正常提交的),小刚的记录已回滚
3.4 enter 返回 self 的用法
class Countdown:
"""倒计时上下文管理器 —— __enter__ 返回自身"""
def __init__(self, start):
self.start = start
self.current = start
def __enter__(self):
print(f"倒计时开始:{self.start}")
return self # 返回自身,调用方可以通过 as 拿到实例
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"倒计时结束(已数到 {self.current})")
return False
def tick(self):
"""手动推进倒计时"""
if self.current > 0:
print(f" {self.current}...")
self.current -= 1
return True
print(" 时间到!")
return False
# 使用:as 拿到的是 Countdown 实例本身
with Countdown(3) as cd:
cd.tick() # 3...
cd.tick() # 2...
cd.tick() # 1...
# 输出:
# 倒计时开始:3
# 3...
# 2...
# 1...
# 倒计时结束(已数到 0)
新手易踩坑:
- 坑 1:
__exit__方法忘了写第三个参数 → 参数数量不匹配,调用时报TypeError- 坑 2:
__exit__中return False写成了return→return等同于return None,None在布尔上下文中是False,所以效果一样,但不够明确- 坑 3:在
__enter__里初始化了资源但在__exit__里没有释放 → 资源泄漏,背离上下文管理器的设计初衷
课堂小练习 2
自己写一个 ReversibleList 上下文管理器:进入时打印列表内容,退出时自动将列表反转。__enter__ 返回自身。
# 期望效果:
with ReversibleList([1, 2, 3, 4, 5]) as rl:
print("处理数据...")
# 输出:
# 进入:[1, 2, 3, 4, 5]
# 处理数据...
# 退出(已反转):[5, 4, 3, 2, 1]
点击查看参考答案
class ReversibleList:
def __init__(self, data):
self.data = data
def __enter__(self):
print(f"进入:{self.data}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.data.reverse()
print(f"退出(已反转):{self.data}")
return False
with ReversibleList([1, 2, 3, 4, 5]) as rl:
print("处理数据...")
四、exit 的三个异常参数详解
4.1 三个参数的含义
__exit__(self, exc_type, exc_val, exc_tb) 的三个参数:
| 参数 | 含义 | with 块没出错时 | with 块出错时 |
|---|---|---|---|
exc_type |
异常的类型 | None |
例如 ValueError |
exc_val |
异常的实例(异常值) | None |
例如 ValueError("负数") |
exc_tb |
Traceback 对象(调用栈) | None |
完整的 traceback 信息 |
4.2 返回值控制异常传播
class ExceptionDemo:
"""演示 __exit__ 返回值对异常传播的影响"""
def __enter__(self):
print("[进入]")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"[退出] exc_type={exc_type}, exc_val={exc_val}")
# === 情况1:返回 False → 异常继续向上传播(默认行为) ===
# return False
# === 情况2:返回 True → 吞掉异常,外部感知不到 ===
return True
# 测试:__exit__ 返回 True 会吞掉异常
print("=== 测试:异常被吞掉 ===")
with ExceptionDemo():
print(" with 块执行中...")
raise ValueError("一个错误!")
print("这行竟然被执行了!因为异常被 __exit__ 吞掉了。")
# 输出:
# === 测试:异常被吞掉 ===
# [进入]
# with 块执行中...
# [退出] exc_type=<class 'ValueError'>, exc_val=一个错误!
# 这行竟然被执行了!因为异常被 __exit__ 吞掉了。
4.3 实战:选择性吞掉某些异常
class SuppressKeyError:
"""只吞掉 KeyError,其他异常正常抛出"""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is KeyError:
print(f"[已抑制] KeyError: {exc_val}")
return True # 吞掉 KeyError
# 其他异常 → 返回 False,继续向上抛
return False
# 测试
print("=== 测试1:KeyError 被吞掉 ===")
with SuppressKeyError():
data = {"name": "小明"}
print(data["age"]) # 触发 KeyError —— 被吞掉了
print("程序继续运行!\n")
print("=== 测试2:ValueError 正常抛出 ===")
try:
with SuppressKeyError():
raise ValueError("其他错误") # ValueError —— 不会被吞
except ValueError as e:
print(f"外部捕获:{e}")
# 输出:
# === 测试1:KeyError 被吞掉 ===
# [已抑制] KeyError: 'age'
# 程序继续运行!
#
# === 测试2:ValueError 正常抛出 ===
# 外部捕获:其他错误
4.4 exit 中访问异常信息
import traceback
class ErrorLogger:
"""记录异常详细信息的上下文管理器"""
def __init__(self, log_file="error.log"):
self.log_file = log_file
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
# 发生了异常,记录详细信息
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(f"[异常类型] {exc_type.__name__}\n")
f.write(f"[异常信息] {exc_val}\n")
f.write(f"[调用栈]\n")
# traceback.format_tb() 获取格式化的调用栈
traceback.print_tb(exc_tb, file=f)
f.write("-" * 40 + "\n")
print(f"异常已记录到 {self.log_file}")
return False # 异常继续传播,不吞掉
# 使用
def calculate(a, b):
return a / b
def process():
with ErrorLogger():
calculate(10, 0) # 故意除以零
try:
process()
except ZeroDivisionError:
print("外部捕获了 ZeroDivisionError")
# error.log 文件内容:
# [异常类型] ZeroDivisionError
# [异常信息] division by zero
# [调用栈]
# File "xxx.py", line xx, in process
# calculate(10, 0)
# File "xxx.py", line xx, in calculate
# return a / b
# ----------------------------------------
4.5 exit 参数总结图解
with 块中的代码执行...
┌─── 没出错 ───→ __exit__(None, None, None)
│ │
│ └──→ 正常退出
│
└─── 出错了 ───→ __exit__(ValueError, ValueError("msg"), traceback)
│
├──→ return True → 吞掉异常,外部无感知
│
└──→ return False → 异常继续向上传播到外部
新手易踩坑:
- 坑 1:
__exit__返回True吞掉了所有异常 → 调试时完全不知道哪里出错了!- 坑 2:在
__exit__里重新抛出异常(raise)→ 会覆盖原始异常,让调试变得更困难- 坑 3:
__exit__中的代码本身可能抛异常 → 如果在关闭资源时出错,Python 会优先抛出__exit__中的异常,原始的异常信息可能丢失
课堂小练习 3
写一个 LogIfError 上下文管理器:只有当 with 块发生异常时,才将异常类型和异常信息打印出来。异常照常传播,不吞掉。
点击查看参考答案
class LogIfError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"[异常日志] {exc_type.__name__}: {exc_val}")
return False # 不吞异常
# 测试
with LogIfError():
print("正常操作,不会有日志")
# (无日志输出)
with LogIfError():
raise RuntimeError("测试异常")
# [异常日志] RuntimeError: 测试异常
# 异常继续传播...
五、contextlib 标准库工具
contextlib 模块提供了更便捷的方式来创建和使用上下文管理器,不需要每次都写一个完整的类。
5.1 @contextmanager:用生成器实现上下文管理器
这是最常用的工具——用一个生成器函数就能创建上下文管理器,代码量减少 80%。
from contextlib import contextmanager
@contextmanager
def managed_file(filename, mode="r", encoding="utf-8"):
"""用生成器实现的文件上下文管理器 —— 无需写类!"""
print(f"[打开] {filename}")
f = open(filename, mode, encoding=encoding) # __enter__ 的逻辑
try:
yield f # 返回资源给 with 块
finally:
f.close() # __exit__ 的逻辑
print(f"[关闭] {filename}")
# 用法和类实现的完全一样!
with managed_file("test.txt", "w") as f:
f.write("用生成器实现的上下文管理器!")
with managed_file("test.txt", "r") as f:
print(f.read())
@contextmanager 的工作原理:
@contextmanager 装饰的生成器函数:
yield 之前的代码 → 等价于 __enter__
│
yield 后面的值 → 等价于 __enter__ 的返回值(赋给 as)
│
yield 之后的代码 → 等价于 __exit__(放在 finally 中确保执行)
执行流程:
┌─────────────────────────────────┐
│ 生成器开始执行 │
│ ├── yield 之前的代码(准备资源) │ ← __enter__
│ ├── yield 资源 → 交给 with 块 │
│ │ (函数在此暂停) │
│ ├── with 块执行... │
│ ├── with 块结束 │
│ ├── yield 之后的代码(清理资源) │ ← __exit__
│ └── 生成器结束 │
└─────────────────────────────────┘
5.2 @contextmanager 的异常处理
from contextlib import contextmanager
@contextmanager
def safe_operation(name):
"""演示 @contextmanager 中的异常处理"""
print(f"[开始] {name}")
try:
yield f"资源-{name}" # 正常返回资源
except ValueError as e:
# 如果捕获了异常,就不会向上传播(相当于 __exit__ 返回 True)
print(f"[捕获] 在上下文管理器内部处理了:{e}")
# 不重新 raise → 异常被吞掉
except Exception:
# 其他异常 → 在 finally 之前重新抛出
print("[发现] 其他类型异常,重新抛出")
raise
finally:
# finally 块中的代码无论异常与否都会执行
print(f"[结束] {name}")
# 测试1:正常情况
print("=== 测试1:正常 ===")
with safe_operation("测试1") as res:
print(f" 使用 {res}")
# 输出:
# === 测试1:正常 ===
# [开始] 测试1
# 使用 资源-测试1
# [结束] 测试1
# 测试2:ValueError 被吞掉
print("\n=== 测试2:ValueError ===")
with safe_operation("测试2") as res:
print(f" 使用 {res}")
raise ValueError("值错误")
print("ValueError 被吞掉了,程序继续!")
# 测试3:其他异常正常传播
print("\n=== 测试3:RuntimeError ===")
try:
with safe_operation("测试3") as res:
raise RuntimeError("运行时错误")
except RuntimeError as e:
print(f"外部捕获:{e}")
# 输出:
# [开始] 测试3
# [发现] 其他类型异常,重新抛出
# [结束] 测试3
# 外部捕获:运行时错误
5.3 常见错误:忘了 try/finally
from contextlib import contextmanager
# 错误写法:没有 try/finally
@contextmanager
def broken_manager(filename):
f = open(filename, "w")
yield f
f.close() # 如果 with 块中出现异常,这行不会执行!
# 正确写法:必须用 try/finally
@contextmanager
def correct_manager(filename):
f = open(filename, "w")
try:
yield f
finally:
f.close() # 无论如何都会执行
新手易踩坑:
- 坑 1:
@contextmanager里yield后面的代码没用try/finally包裹 → 出错时资源不会释放- 坑 2:
@contextmanager里yield只能出现一次 → 生成器只能yield一个值- 坑 3:
yield返回的值赋给了as后面的变量,yield本身没有值
5.4 closing():给有 close() 的对象"披上"上下文管理器外衣
很多第三方库的对象有 close() 方法但没有实现 __enter__/__exit__。closing() 就是为这些对象服务的。
from contextlib import closing
from urllib.request import urlopen
# urlopen 返回的对象有 close() 但没有实现上下文管理协议(旧版本)
# closing() 帮它"补上"这个能力
# 不推荐(旧式写法):
response = urlopen("https://httpbin.org/get")
try:
html = response.read()
finally:
response.close()
# 推荐(用 closing 包装):
with closing(urlopen("https://httpbin.org/get")) as response:
html = response.read()
# 退出 with 时自动调用 response.close()
# closing 的本质(源码级简化版):
# class closing:
# def __init__(self, thing):
# self.thing = thing
# def __enter__(self):
# return self.thing
# def __exit__(self, *args):
# self.thing.close()
5.5 suppress():优雅地忽略指定异常
from contextlib import suppress
# 传统写法:用 try/except 忽略 KeyError
data = {"name": "小明"}
try:
print(data["age"])
except KeyError:
pass # 吞掉异常,什么都不做
# suppress 写法:更简洁
data = {"name": "小明"}
with suppress(KeyError):
print(data["age"]) # KeyError 被吞掉,程序继续
# 实战:安全地删除文件(文件不存在不报错)
import os
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("maybe_not_exist.txt")
# 实战:安全地关闭多个网络连接
with suppress(OSError):
socket1.close()
with suppress(OSError):
socket2.close()
# 可以传入多个异常类型
with suppress(KeyError, IndexError, TypeError):
value = some_risky_operation()
5.6 ContextDecorator:既是上下文管理器又是装饰器
from contextlib import ContextDecorator
class log_execution(ContextDecorator):
"""既可以当上下文管理器用,也可以当装饰器用!"""
def __init__(self, prefix=""):
self.prefix = prefix
def __enter__(self):
print(f"{self.prefix}[进入]")
return self
def __exit__(self, *args):
print(f"{self.prefix}[退出]")
# 用法1:上下文管理器
with log_execution("[CM] "):
print(" 执行操作...")
# 输出:
# [CM] [进入]
# 执行操作...
# [CM] [退出]
# 用法2:装饰器(整个函数自动包裹在 with 中!)
@log_execution("[装饰] ")
def my_function():
print(" 函数内容")
my_function()
# 输出:
# [装饰] [进入]
# 函数内容
# [装饰] [退出]
5.7 redirect_stdout / redirect_stderr:重定向标准输出
from contextlib import redirect_stdout
import io
# 场景:捕获 print 的输出,而不是让它打印到控制台
f = io.StringIO() # 一个内存中的"文件"
with redirect_stdout(f): # 把 print 的输出重定向到 f
print("这行不会显示在控制台")
print("这行也不会")
print("所有的 print 都进了 StringIO")
output = f.getvalue() # 获取被截获的输出
print("=== 截获的输出 ===")
print(output)
# 输出:
# === 截获的输出 ===
# 这行不会显示在控制台
# 这行也不会
# 所有的 print 都进了 StringIO
5.8 contextlib 工具速查表
| 工具 | 作用 | 典型场景 |
|---|---|---|
@contextmanager |
用生成器快速创建上下文管理器 | 不需要写完整类时 |
closing(thing) |
给有 close() 的对象加上下文管理 |
第三方库对象 |
suppress(*exceptions) |
忽略指定异常 | 文件删除、资源关闭 |
ContextDecorator |
上下文管理 + 装饰器二合一 | 需要两种用法的工具 |
redirect_stdout(f) |
重定向标准输出 | 捕获 print 输出 |
redirect_stderr(f) |
重定向标准错误 | 捕获错误输出 |
ExitStack |
动态管理多个上下文管理器 | 不确定数量的资源管理(进阶) |
课堂小练习 4
用 @contextmanager 实现一个 timer 上下文管理器,统计 with 块中代码的运行时间(秒)。
点击查看参考答案
from contextlib import contextmanager
import time
@contextmanager
def timer(name="代码块"):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"[{name}] 耗时:{elapsed:.4f} 秒")
with timer("计算测试"):
total = sum(range(10000000))
# 输出:[计算测试] 耗时:0.2158 秒
六、综合实战案例
案例 1:代码计时上下文管理器(完整版)
需求:统计 with 块内代码的执行时间,支持命名、支持获取详细统计。
import time
from contextlib import contextmanager
class Timer:
"""高性能计时器 —— 统计代码块运行时间"""
def __init__(self, name="代码块"):
self.name = name
self.start_time = None
self.end_time = None
self.elapsed = None
def __enter__(self):
# 使用 perf_counter 而非 time.time(精度更高,不受系统时间调整影响)
self.start_time = time.perf_counter()
return self # 返回自身,外部可以获取详细数据
def __exit__(self, *args):
self.end_time = time.perf_counter()
self.elapsed = self.end_time - self.start_time
# 格式化输出(自动选择合适的单位)
if self.elapsed < 0.001:
print(f"[Timer] {self.name}: {self.elapsed * 1_000_000:.2f} μs")
elif self.elapsed < 1:
print(f"[Timer] {self.name}: {self.elapsed * 1000:.2f} ms")
else:
print(f"[Timer] {self.name}: {self.elapsed:.4f} s")
return False # 不吞异常
# === 使用演示 ===
with Timer("大数据排序") as t:
data = list(range(10000000, 0, -1))
data.sort()
# 可以事后查看 t.elapsed 属性
# [Timer] 大数据排序: 0.3241 s
with Timer("快速操作"):
result = sum(range(1000))
# [Timer] 快速操作: 0.12 ms
案例 2:临时文件自动清理上下文管理器
需求:在 with 块内创建一个临时文件(或目录),退出时自动删除,无论代码是否出错。
import os
import tempfile
from pathlib import Path
class TemporaryWorkspace:
"""临时工作目录 —— 退出时自动清理所有内容"""
def __init__(self, prefix="workspace_", keep_on_error=False):
"""
prefix: 临时目录名前缀
keep_on_error: 如果 with 块出错,是否保留目录(调试用)
"""
self.prefix = prefix
self.keep_on_error = keep_on_error
self.path = None
def __enter__(self):
# 创建临时目录
self.path = Path(tempfile.mkdtemp(prefix=self.prefix))
print(f"[创建] 临时目录:{self.path}")
return self.path
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None and self.keep_on_error:
# 出错了但不删除——方便事后查看中间产物
print(f"[保留] 因出错保留目录:{self.path}")
return False
# 递归删除整个目录及其所有内容
import shutil
shutil.rmtree(self.path, ignore_errors=True)
print(f"[清理] 临时目录已删除:{self.path}")
return False
# === 使用演示 ===
# 场景1:正常使用
with TemporaryWorkspace("data_") as ws:
# 在临时目录中创建文件
(ws / "input.txt").write_text("临时数据", encoding="utf-8")
(ws / "output.csv").write_text("列1,列2\n1,2", encoding="utf-8")
# 列出临时目录内容
print("临时目录中的文件:")
for f in ws.iterdir():
print(f" - {f.name}")
# 输出:
# [创建] 临时目录:C:\Users\...\Temp\data_xxxxxx
# 临时目录中的文件:
# - input.txt
# - output.csv
# [清理] 临时目录已删除:C:\Users\...\Temp\data_xxxxxx
# 场景2:出错时保留现场
with TemporaryWorkspace("debug_", keep_on_error=True) as ws:
(ws / "partial_result.txt").write_text("部分数据", encoding="utf-8")
raise RuntimeError("处理过程中出错!")
# 输出:
# [创建] 临时目录:C:\Users\...\Temp\debug_xxxxxx
# [保留] 因出错保留目录:C:\Users\...\Temp\debug_xxxxxx
# (RuntimeError 继续传播)
案例 3:异常捕获日志记录上下文管理器
需求:包裹一段代码,捕获指定类型的异常,记录日志,支持重试。
import time
import logging
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
class ErrorMonitor:
"""
异常监控上下文管理器
功能:
1. 捕获指定类型的异常并记录日志
2. 支持自动重试(retry 次)
3. 记录每次重试的详细信息
"""
def __init__(self, operation_name, retry=0, delay=1,
catch=(Exception,), reraise=True):
"""
operation_name: 操作名称(用于日志)
retry: 失败后重试次数
delay: 重试间隔(秒)
catch: 要捕获的异常类型(元组)
reraise: 重试耗尽后是否重新抛出异常
"""
self.name = operation_name
self.retry = retry
self.delay = delay
self.catch = catch
self.reraise = reraise
self.attempt = 0
self.final_error = None
def __enter__(self):
print(f"\n{'='*50}")
print(f"[操作] {self.name}")
print(f"[时间] {datetime.now().strftime('%H:%M:%S')}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
print(f"[结果] 操作成功 ✓")
print(f"{'='*50}\n")
return False
# 判断是否是我们要捕获的异常类型
if issubclass(exc_type, self.catch):
self.attempt += 1
print(f"[失败] 第 {self.attempt} 次尝试:{exc_type.__name__}: {exc_val}")
# 还有重试次数 → 吞掉异常,让外部代码进入下一次尝试
if self.attempt <= self.retry:
print(f"[重试] 等待 {self.delay} 秒后重试...")
time.sleep(self.delay)
return True # 吞掉异常,外部循环继续
# 重试耗尽
self.final_error = exc_val
print(f"[放弃] 已重试 {self.retry} 次,仍然失败")
print(f"{'='*50}\n")
if self.reraise:
return False # 让异常继续向上传播
else:
return True # 彻底吞掉
# 不是我们要捕获的类型 → 照常传播
print(f"[不可处理] {exc_type.__name__}(不在捕获范围内)")
print(f"{'='*50}\n")
return False
# === 使用演示 ===
import random
def flaky_operation():
"""模拟不稳定的操作(40% 成功率)"""
if random.random() < 0.4:
return "成功"
raise ConnectionError("网络连接超时")
# 最多重试 3 次,每次间隔 1 秒
for _ in range(5): # 外层最多尝试 5 次
with ErrorMonitor("网络数据获取", retry=3, delay=1,
catch=(ConnectionError, TimeoutError)) as monitor:
result = flaky_operation()
print(f"[数据] {result}")
break # 成功就退出循环
else:
print("最终放弃:操作仍然失败")
运行效果演示:
==================================================
[操作] 网络数据获取
[时间] 14:30:00
[失败] 第 1 次尝试:ConnectionError: 网络连接超时
[重试] 等待 1 秒后重试...
==================================================
[操作] 网络数据获取
[时间] 14:30:01
[失败] 第 2 次尝试:ConnectionError: 网络连接超时
[重试] 等待 1 秒后重试...
==================================================
[操作] 网络数据获取
[时间] 14:30:02
[数据] 成功
[结果] 操作成功 ✓
==================================================
案例 4:组合多个上下文管理器的高级用法
from contextlib import contextmanager, redirect_stdout
import io
import time
@contextmanager
def measure_and_capture(name="操作"):
"""
组合上下文管理器:
1. 计时(Timer 逻辑)
2. 捕获 print 输出
"""
f = io.StringIO()
start = time.perf_counter()
try:
# 嵌套两个上下文:redirect_stdout 在内部
with redirect_stdout(f):
yield f # 把 StringIO 传给 with 块
finally:
elapsed = time.perf_counter() - start
output = f.getvalue()
print(f"[{name}] 耗时 {elapsed:.4f}s")
if output.strip():
print(f"[{name}] 产生的输出:")
for line in output.strip().split("\n"):
print(f" │ {line}")
# 使用
with measure_and_capture("数据处理") as captured:
print("步骤1:加载数据...")
total = sum(range(1000000))
print(f"步骤2:计算结果 = {total}")
# 输出:
# [数据处理] 耗时 0.0412s
# [数据处理] 产生的输出:
# │ 步骤1:加载数据...
# │ 步骤2:计算结果 = 499999500000
课堂小练习 5
实现一个 Transaction 上下文管理器,模拟银行转账的事务处理:
- 进入时打印"开始事务"
- 用
__exit__的参数判断是否有异常 - 无异常时打印"提交事务",有异常时打印"回滚事务" + 异常原因
- 异常照常传播
点击查看参考答案
class Transaction:
def __init__(self, name="事务"):
self.name = name
def __enter__(self):
print(f"[{self.name}] 开始")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
print(f"[{self.name}] 提交 ✓")
else:
print(f"[{self.name}] 回滚 ✗ ({exc_type.__name__}: {exc_val})")
return False # 异常继续传播
# 正常情况
with Transaction("转账"):
print(" 张三 -100")
print(" 李四 +100")
# [转账] 开始
# 张三 -100
# 李四 +100
# [转账] 提交 ✓
# 异常情况
with Transaction("转账"):
print(" 张三 -100")
raise ValueError("李四账户不存在!")
# [转账] 开始
# 张三 -100
# [转账] 回滚 ✗ (ValueError: 李四账户不存在!)
七、常见误区与面试题
7.1 易错点汇总
错误 1:把 with 当成"作用域"来限制变量访问
# 错误理解:以为出了 with 就不能用 f 了
with open("test.txt") as f:
content = f.read()
# f 仍然存在!只是 f.close() 已经被调用了
print(f.closed) # True —— 文件已关闭
# f.read() # ValueError: I/O operation on closed file.
# 如果调用 f.read(),会报错——不是变量不存在,而是文件已关闭
错误 2:在 @contextmanager 中忘了 try/finally
# 错误:yield 后的代码不在 finally 中
@contextmanager
def bad_manager(filename):
f = open(filename)
yield f
f.close() # 如果 yield 处抛出异常,这行不会执行!
# 正确:
@contextmanager
def good_manager(filename):
f = open(filename)
try:
yield f
finally:
f.close() # 无论如何都会执行
错误 3:exit 吞掉异常但不做任何处理
class DangerousManager:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return True # 吞掉所有异常!调试时完全不知道问题在哪
# 正确:除非你清楚自己在做什么,否则 return False
错误 4:混淆 with 和 try/except 的作用
# with 的作用是管理资源(自动释放),不是捕获异常
# try/except 的作用才是捕获异常
# 错误期望:以为 with 会捕获 FileNotFoundError
with open("not_exist.txt") as f: # FileNotFoundError 仍然会抛出!
pass
# 需要自己加 try/except
# 正确:
try:
with open("not_exist.txt") as f:
pass
except FileNotFoundError:
print("文件不存在")
错误 5:在 enter 里分配了资源但在 exit 出错时不处理
class BadConnection:
def __enter__(self):
self.resource1 = acquire_resource1() # 分配成功
self.resource2 = acquire_resource2() # 如果这里出错...
return self
# 问题:如果 resource2 分配失败,resource1 不会被释放!
# 正确做法:分步处理
class GoodConnection:
def __enter__(self):
self.resource1 = acquire_resource1()
try:
self.resource2 = acquire_resource2()
except:
self.resource1.close() # 失败时手动释放 resource1
raise
return self
7.2 经典面试题
题 1:以下代码输出什么?
class Test:
def __enter__(self):
print("A")
return self
def __exit__(self, *args):
print("B")
return True
with Test():
print("C")
print("D")
答案
A
C
B
D
解析:__enter__ → with 块 → __exit__;因为 __exit__ 返回 True 但没有异常发生,不影响后续代码。
题 2:以下代码输出什么?
class Test:
def __enter__(self):
print("A")
return self
def __exit__(self, *args):
print("B")
print(f"异常类型: {args[0]}")
return True # 吞掉异常
with Test():
print("C")
raise ValueError("出错了")
print("D")
答案
A
C
B
异常类型: <class 'ValueError'>
D
解析:__exit__ 的 args[0] 是 ValueError,返回 True 吞掉异常,所以 print("D") 会执行。
如果改为 return False,则 print("D") 不会执行(异常传播出去)。
题 3:@contextmanager 装饰的生成器中,yield 可以出现多次吗?
答案
不可以! @contextmanager 装饰的生成器中 yield 只能出现一次。
原因:with 语句只期望 __enter__ 返回一个资源。多次 yield 会导致 RuntimeError。
from contextlib import contextmanager
@contextmanager
def broken():
yield 1
yield 2 # RuntimeError: generator didn't stop
# 如果确实需要多个值,用元组或自定义对象包装:
@contextmanager
def fixed():
yield (1, 2) # 把多个值打包返回
with fixed() as (a, b):
print(a, b) # 1 2
题 4:如何让一个自定义上下文管理器同时支持 with 语句和 async with 语句?
答案
需要分别实现同步和异步的协议方法:
class DualManager:
def __enter__(self):
print("同步 __enter__")
return self
def __exit__(self, *args):
print("同步 __exit__")
async def __aenter__(self):
print("异步 __aenter__")
return self
async def __aexit__(self, *args):
print("异步 __aexit__")
# 同步用法
with DualManager():
pass
# 异步用法
# async with DualManager():
# pass
总结
上下文管理器知识地图:
with 语句
(自动管理资源)
│
┌─────────────┼─────────────┐
↓ ↓ ↓
__enter__ with 代码块 __exit__
(获取资源) (使用资源) (释放资源)
│ │
│ ┌────────────┤
│ ↓ ↓
│ 没出错→(None,...) 出错了→(异常信息)
│ │ │
│ │ return True 吞掉
│ │ return False 传播
│ │
└────── 返回值→ as 变量
核心心法三句话:
- with = 自动借还:你只管用,
__exit__帮你自动归还 - @contextmanager = 简化版:用生成器替代类,yield 前 =
__enter__,yield 后 =__exit__ - **exit 返回 True 吞异常,False 传异常**:正常情况下用
False,除非你明确知道要吞掉
评论区