with 语句是 Python 中用于资源管理上下文控制的关键特性。它的核心目标是:确保代码在进入和退出某个代码块时,能够可靠地执行预设的设置和清理操作,即使代码块内部抛出异常也能保证清理。这大大简化了传统的 try/finally 写法,使代码更简洁、安全、易读。

1. 传统方式 vs with 语句

传统文件操作(容易忘记关闭文件,或异常时无法关闭):

f = open('data.txt', 'r')
try:
    content = f.read()
    # 处理数据...
finally:
    f.close()          # 必须手动关闭,但容易遗漏

使用 with

with open('data.txt', 'r') as f:
    content = f.read()
# 离开 with 块时,文件自动关闭,即使发生异常

2. 上下文管理器协议

with 语句依赖上下文管理器(Context Manager)对象,该对象必须实现两个特殊方法:

  • __enter__(self) → 进入上下文时调用,返回值会赋值给 as 后面的变量(如果没有 as 则忽略)。
  • __exit__(self, exc_type, exc_val, exc_tb) → 离开上下文时调用,用于清理资源。如果发生异常,异常信息会通过三个参数传入;如果没有异常,三个参数均为 None。该方法返回 True 表示已处理异常,异常不再向外传播;返回 False(默认)或 None 则异常会继续抛出。

执行流程

with context_manager as target:
    # 代码块
  1. 调用 context_manager.__enter__(),结果赋给 target(若有 as)。
  2. 执行代码块。
  3. 如果代码块正常结束或抛出异常,都调用 __exit__(exc_type, exc_val, exc_tb)
  4. 根据 __exit__ 的返回值决定是否传播异常。

3. 内置的上下文管理器

Python 许多内置对象都支持上下文管理:

  • 文件open('file') → 自动关闭
  • 线程锁threading.Lock → 自动获取和释放
  • decimal 上下文decimal.localcontext() → 临时精度设置
  • 临时目录tempfile.TemporaryDirectory() → 自动删除
  • 重定向标准输出contextlib.redirect_stdout
  • 数据库连接:部分驱动(如 sqlite3.Connection)实现了上下文,自动提交/回滚

示例:锁

import threading
lock = threading.Lock()
with lock:
    # 自动获取锁,离开时释放
    critical_section()

4. 自定义上下文管理器(两种主流方法)

4.1 类实现协议
class ManagedFile:
    def __init__(self, filename, mode='r'):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file   # 返回给 as 变量

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        # 如果发生异常,可以记录或处理;这里返回 False 让异常继续传播
        return False

# 使用
with ManagedFile('test.txt', 'w') as f:
    f.write('hello')
4.2 使用 contextlib.contextmanager 装饰器(生成器方式)

这种方法用 yield 分隔进入和退出逻辑,代码更简洁。

from contextlib import contextmanager

@contextmanager
def managed_file(filename, mode='r'):
    f = open(filename, mode)
    try:
        yield f          # 进入代码块,f 赋给 as 变量
    finally:
        f.close()        # 无论异常与否,都会执行

with managed_file('test.txt', 'w') as f:
    f.write('hello')

注意:生成器内部必须使用 try/finallywith 语句保证资源释放,因为 yield 可能会抛出异常。@contextmanager 会捕获 yield 处的异常并传递给 __exit__ 逻辑。

5. 异常处理与 __exit__ 的返回值

__exit__ 方法可以决定是否吞掉异常:

class SuppressExceptions:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            print(f"捕获到异常:{exc_val}")
            return True   # 异常已处理,不再向外抛出

with SuppressExceptions():
    raise ValueError("故意出错")
print("程序继续执行")   # 会打印

如果不希望吞掉异常,返回 FalseNone,或者让 __exit__ 隐式返回 None

6. 同时管理多个上下文

Python 3.1+ 支持多个上下文管理器并列写:

with open('in.txt') as inf, open('out.txt', 'w') as outf:
    outf.write(inf.read())

Python 3.10+ 支持括号内换行:

with (
    open('in.txt') as inf,
    open('out.txt', 'w') as outf
):
    outf.write(inf.read())

7. 高级技巧与注意事项

7.1 重写 __enter__ 的返回值

可以返回不同的对象,不一定是上下文管理器本身。比如数据库连接对象在 __enter__ 中返回游标。

7.2 contextlib.ExitStack – 动态管理多个上下文

当需要在循环或条件中动态添加上下文管理器时,ExitStack 非常有用。

from contextlib import ExitStack

files = ['a.txt', 'b.txt', 'c.txt']
with ExitStack() as stack:
    fhandles = [stack.enter_context(open(fname)) for fname in files]
    # 所有文件会在 with 块结束时自动关闭
7.3 异步上下文管理器(async with

用于异步资源管理,协议为 __aenter__ / __aexit__,例如异步文件操作、数据库连接等。

async with aiofiles.open('data.txt') as f:
    content = await f.read()
7.4 contextlib.closing – 适配无协议但有 close 方法的对象

如果某个对象实现了 close() 但没有 __enter__/__exit__,可以用 closing 包装:

from contextlib import closing
import urllib.request

with closing(urllib.request.urlopen('http://example.com')) as page:
    html = page.read()
# 自动调用 page.close()
7.5 contextlib.nullcontext – 条件性使用上下文

有时需要根据条件决定是否进入上下文(例如测试时跳过锁),可以使用 nullcontext 作为占位:

from contextlib import nullcontext

ctx = lock if need_lock else nullcontext()
with ctx:
    # 如果有锁,自动获取释放;否则什么都不做

8. 典型应用场景总结

场景 资源 清理操作
文件读写 文件描述符 关闭文件
线程锁 锁对象 释放锁
数据库连接 连接对象 关闭连接/回滚事务
临时文件/目录 临时资源 删除临时对象
重定向 stdout/stderr 标准流 恢复原始流
计时器 性能计数器 输出耗时

9. 性能与陷阱

  • 性能with 本身开销很小,比手动 try/finally 更简洁且不易出错。
  • 陷阱:在生成器上下文(@contextmanager)中,如果 yield 后发生异常且未捕获,资源可能无法正确清理。务必使用 try/finally
  • 陷阱__exit__ 中如果自己抛出异常,会覆盖原始异常,除非你明确需要替换异常。

10. 总结

with 语句是 Python 优雅处理资源的基石,核心是上下文管理器协议。通过实现 __enter__ / __exit__ 或使用 @contextmanager 装饰器,你可以将任何资源封装为安全的上下文。它适用于文件、锁、网络连接、数据库事务等几乎所有需要“设置-清理”模式的场景。掌握 with 可以显著提高代码的健壮性和可读性。

更多推荐