22·模块六:文件与IO进阶

上下文管理器(with 语句)

context-managerswith-statement

Python 编程:从零基础到精通

第 22 课 - 上下文管理器(with 语句)

所属模块: 模块六:文件与IO 难度: intermediate 标签: context-managers, with-statement

学习目标

  1. 理解上下文管理器的核心概念:明白它是用于管理资源(如文件、网络连接、锁等)生命周期的协议。
  2. 熟练掌握 with 语句:能够使用 with 语句安全、优雅地操作文件或其他资源。
  3. 能够自定义上下文管理器:学会通过实现 __enter____exit__ 方法,以及使用 @contextmanager 装饰器来创建自己的上下文管理器。
  4. 了解常见应用场景:识别在什么情况下使用上下文管理器可以提高代码的健壮性和可读性。

核心概念

想象一下你是一个临时工,需要借用公司的工具间拿一些工具。传统做法是:开门 -> 拿工具 -> 无论如何,最后都要记得锁门。如果中间出了什么差错(比如拿着工具时滑倒了),你可能会忘记锁门,导致问题。

上下文管理器就像一个自动门禁系统。你只需要说:"我要进入这个工具间"(with 语句),进去后自动获取访问权__enter__),无论你在这个空间里做什么(正常操作或发生异常),离开时系统都会自动确保门被锁好__exit__)。你再也不用担心忘记"锁门"这一步了。

在编程中,"资源"就是文件、数据库连接、网络套接字等需要显式"打开"和"关闭"的对象。with 语句的语法如下:

with 表达式 as 变量:
    代码块

这个"表达式"会返回一个上下文管理器对象。with 语句确保在进入代码块时调用管理器的 __enter__ 方法(其返回值赋给 as 后的变量),并在退出代码块(无论正常结束还是发生异常)时调用 __exit__ 方法。这保证了资源的正确释放。


代码示例

示例 1:基本文件操作——从 try-finally 到 with

传统写法需要显式关闭文件,且容易在异常时出错。

# 传统写法(不推荐)
f = open('example.txt', 'w')
try:
    f.write('Hello, Context Manager!')
    # 可能会发生异常的其他操作...
    f.write('\nThis is the second line.')
finally:
    print("执行 finally 块:确保文件被关闭。")
    f.close()  # 必须显式关闭

使用 with 语句,代码更简洁、安全。

# 推荐的现代写法
with open('example.txt', 'w') as f:
    f.write('Hello, Context Manager!')
    # 可能会发生异常的其他操作...
    f.write('\nThis is the second line.')
# 离开 with 块后,文件会自动关闭,即使发生异常。
print(f"文件状态:{f.closed}")  # 输出: True

示例 2:自定义上下文管理器(类)

我们可以通过实现 __enter____exit__ 方法来创建自定义的上下文管理器。

class MyContext:
    """一个简单的自定义上下文管理器示例"""
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        """进入 with 语句时调用。返回值赋给 as 后的变量。"""
        print(f"进入上下文:{self.name}")
        return self  # 通常返回自身,以便在代码块中使用

    def __exit__(self, exc_type, exc_val, exc_tb):
        """退出 with 语句时调用。负责清理工作。
        参数:exc_type, exc_val, exc_tb 记录了代码块中发生的异常信息(如果有)。
        """
        print(f"退出上下文:{self.name}")
        if exc_type:
            print(f"处理异常:{exc_val}")
            # 返回 True 表示异常已被处理,不再向上抛出。
            # 返回 False(或 None)则表示异常会继续传播。
            return True
        return False

# 使用自定义上下文管理器
print("--- 正常情况 ---")
with MyContext("资源A") as mc:
    print(f"在 with 块中,名称是: {mc.name}")
    print("执行一些操作...")

print("\n--- 发生异常的情况 ---")
with MyContext("资源B") as mc:
    print("准备发生异常...")
    raise ValueError("模拟一个错误!")  # 触发异常
    print("这行不会被执行。")
# 注意:__exit__ 方法捕获了异常并返回 True,所以程序没有崩溃。

示例 3:使用 contextlib.contextmanager 装饰器

编写一个类有时过于繁琐。对于简单的场景,contextlib 模块提供了更简洁的方式。

from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    """使用生成器函数定义的上下文管理器"""
    print(f"[Setup] 获取资源: {name}")
    resource = {"name": name, "data": []}  # 模拟一个资源对象
    try:
        yield resource  # yield 之前是 __enter__ 的逻辑,yield 的值赋给 as 后的变量
    except Exception as e:
        print(f"[Cleanup] 捕获到异常: {e}")
        # 可以选择处理异常或重新抛出
        raise  # 重新抛出异常
    finally:
        print(f"[Cleanup] 释放资源: {name}")
        # 这里做清理工作

# 使用
with managed_resource("DB_Connection") as res:
    print(f"使用资源: {res['name']}")
    res['data'].append(123)
    print(f"资源数据: {res['data']}")

实践练习

练习 1:基础应用

编写一个程序,使用 with 语句同时打开一个文本文件进行读取,和另一个文本文件进行写入,将第一个文件的所有内容(转换为大写)写入第二个文件。

预期输出: 假设输入文件 input.txt 内容为 "hello world",运行后生成 output.txt,内容为 "HELLO WORLD",并且两个文件都自动关闭。

练习 2:自定义管理器

创建一个名为 Timer 的上下文管理器类,用于测量其代码块的执行时间。它应该在进入时记录开始时间,在退出时计算并打印所花费的时间。

要求:

  1. 使用 time 模块的 time 函数。
  2. 支持嵌套使用(提示:考虑使用类变量或实例变量)。

示例用法:

with Timer():
    # 模拟一些耗时操作
    time.sleep(1.5)
    total = sum(range(1000000))

预期输出: Elapsed time: 1.50234 seconds(时间数值会变化)

练习 3:综合应用(进阶)

模拟一个简单的数据库连接。编写一个上下文管理器 DatabaseConnection,它能在进入时"连接"数据库(打印一条消息),在退出时"断开连接"并自动"提交"事务(打印消息)。如果代码块内发生异常,则"回滚"事务(打印消息)。

要求:

  1. 使用 @contextmanager 装饰器实现。
  2. 管理器本身不实现真正的数据库操作,只需打印相应的状态信息。

示例用法与预期输出:

# 正常情况
with DatabaseConnection("MyDB") as conn:
    print("执行查询1...")
    print("执行查询2...")
# 输出:
# [Connect] 正在连接到 MyDB...
# 执行查询1...
# 执行查询2...
# [Disconnect] 正在断开 MyDB 的连接。
# [Commit] 事务已提交。

# 异常情况
with DatabaseConnection("MyDB") as conn:
    print("执行查询1...")
    raise RuntimeError("查询失败!")
    print("执行查询2...")
# 输出:
# [Connect] 正在连接到 MyDB...
# 执行查询1...
# [Disconnect] 正在断开 MyDB 的连接。
# [Rollback] 事务已回滚。
# (程序可能会因未处理的 RuntimeError 而结束,这符合预期)

常见错误

  1. 忽略 with 的优势:在学习后仍然习惯性地使用 try-finally 手动关闭资源。应养成使用 with 语句的习惯,它能让代码更安全、更简洁。
  2. 自定义上下文管理器时 __exit__ 逻辑错误:例如,在 __exit__ 中做了可能引发异常的操作,却没有妥善处理,这会掩盖原始异常。或者错误地返回了 True,导致原本应该暴露的错误被静默吞掉。
  3. 误解 @contextmanager 中的 yield:将 yield 之后的代码(清理逻辑)误认为是代码块执行结束后立刻执行。实际上,它是 finally 块中的代码,无论是否发生异常都会执行。另外,忘记用 try...finally 包裹 yield 可能会导致清理代码不执行。
  4. with 块外使用资源:例如,在 with open(...) as f: 块结束后,再尝试使用变量 f 进行读写操作,这会引发 ValueError: I/O operation on closed file,因为资源(文件)已被自动关闭。

小结

在本课中,我们学习了:

  • 上下文管理器是用于自动管理资源获取和释放的协议,核心是解决了资源泄漏问题。
  • with 语句是使用上下文管理器的语法糖,它保证了即使在发生异常时,清理代码(__exit__)也一定会执行。
  • 自定义上下文管理器有两种主要方式:
    1. :实现 __enter____exit__ 方法。
    2. 生成器函数:使用 @contextmanager 装饰器,通过 yield 将函数分为进入、执行、清理三部分。
  • 上下文管理器不仅用于文件操作,还广泛用于数据库连接、线程锁、网络连接、临时目录等任何需要"成对"操作的资源管理场景。

掌握 with 语句和上下文管理器,是编写健壮、高效、易维护的 Python 代码的关键一步。在下一课,我们将利用这个强大的工具来处理 CSV 和 JSON 等常见格式的文件。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「读写常见格式文件(CSV, JSON)」 以巩固所学知识。