目 录CONTENT

文章目录

Python(三十七) 上下文管理器完整教程

Python(三十七) 上下文管理器完整教程


目录

  1. 开篇:从"借书"理解上下文管理器
  2. with 语句的核心原理
  3. 基于类的上下文管理器:enterexit
  4. exit 的三个异常参数详解
  5. contextlib 标准库工具
  6. 综合实战案例
  7. 常见误区与面试题

一、开篇:从"借书"理解上下文管理器

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__ 方法
  • 坑 2as 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()

三、基于类的上下文管理器:enterexit

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 写成了 returnreturn 等同于 return NoneNone 在布尔上下文中是 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@contextmanageryield 后面的代码没用 try/finally 包裹 → 出错时资源不会释放
  • 坑 2@contextmanageryield 只能出现一次 → 生成器只能 yield 一个值
  • 坑 3yield 返回的值赋给了 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 变量

核心心法三句话:

  1. with = 自动借还:你只管用,__exit__ 帮你自动归还
  2. @contextmanager = 简化版:用生成器替代类,yield 前 = __enter__,yield 后 = __exit__
  3. **exit 返回 True 吞异常,False 传异常**:正常情况下用 False,除非你明确知道要吞掉
0

评论区