Python `with` 语句深入讲解
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:
# 代码块
- 调用
context_manager.__enter__(),结果赋给target(若有as)。 - 执行代码块。
- 如果代码块正常结束或抛出异常,都调用
__exit__(exc_type, exc_val, exc_tb)。 - 根据
__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/finally或with语句保证资源释放,因为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("程序继续执行") # 会打印
如果不希望吞掉异常,返回 False 或 None,或者让 __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 可以显著提高代码的健壮性和可读性。
更多推荐



所有评论(0)