3. 上下文管理器

说实话,刚学Python那会儿,我总觉得with open() as f就是个偷懒的写法。直到有一次线上服务因为文件句柄泄漏挂了,我才真正明白——这玩意儿不是偷懒,是保命用的。

上下文管理器,说白了就是帮你管好资源的。你只管用,它负责开和关。嗯,就像你请了个管家,饭端上来你吃,吃完他收拾,你连碗都不用碰。

with语句:最常用的资源管家

先看最常见的用法:

# 传统写法
f = open('data.txt', 'r')
content = f.read()
f.close()

# with写法
with open('data.txt', 'r') as f:
    content = f.read()

两段代码效果一样,但第二段安全得多。为什么?因为with保证文件一定会被关闭,哪怕中间抛了异常。

核心要点:with语句在进入时调用__enter__,在退出时调用__exit__。不管正常退出还是异常退出,__exit__都会被执行。

我个人习惯,凡是涉及资源操作的,一律用with。文件、网络连接、数据库会话、锁……一个都别放过。

自动资源管理:不止是文件

很多人以为上下文管理器只能管文件,其实它能管的东西多了去了。我列几个常见的:

资源类型 示例 说明
文件 with open(...) as f 自动关闭文件句柄
线程锁 with lock: 自动获取和释放锁
网络连接 with socket.create_connection(...) as s 自动关闭连接
数据库游标 with connection.cursor() as cur 自动提交/回滚事务
临时文件 with tempfile.NamedTemporaryFile() as tf 自动删除临时文件

你想想看,如果每个资源都要手动close(),代码里得塞多少try...finally?而且人总会忘的。我曾经排查过一个bug,查了两天才发现是某个网络连接没关,导致连接池被耗尽。从那以后,我写代码就养成了条件反射——看到资源,先写with。

自定义上下文管理器:自己造管家

Python允许你自定义上下文管理器。有两种方式:

方式一:基于类

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end = time.time()
        print(f'耗时:{self.end - self.start:.3f}秒')
        # 返回False表示不抑制异常,True表示抑制
        return False

# 使用
with Timer() as t:
    time.sleep(1)
    # 输出:耗时:1.001秒

这里要注意__exit__的三个参数:exc_type是异常类型,exc_val是异常实例,exc_tb是回溯信息。如果没有异常,这三个都是None。

避坑指南:__exit__返回True会抑制异常。我曾经在项目里不小心返回了True,结果异常被吞掉了,排查了半天才发现。除非你明确知道自己在做什么,否则建议返回False。

方式二:基于生成器(更简洁)

from contextlib import contextmanager

@contextmanager
def timer():
    start = time.time()
    try:
        yield  # 这里就是with块执行的地方
    finally:
        end = time.time()
        print(f'耗时:{end - start:.3f}秒')

# 使用
with timer():
    time.sleep(1)

我个人更偏爱生成器方式,代码量少,逻辑清晰。但要注意,yield前后的代码分别对应__enter__和__exit__,try...finally保证异常时也能执行清理逻辑。

知识体系总览

下面这张图帮你理清上下文管理器的核心脉络:

上下文管理器知识体系 上下文管理器 with 语句 自动资源管理 自定义管理器 __enter__ __exit__ 异常处理 文件 网络连接 锁/数据库 基于类 @contextmanager yield 用法 核心原则:进入时获取资源,退出时释放资源 异常安全 · 代码简洁 · 避免资源泄漏

进阶技巧:嵌套与组合

多个上下文管理器可以嵌套使用:

# 方式一:嵌套
with open('a.txt') as f1:
    with open('b.txt') as f2:
        data = f1.read() + f2.read()

# 方式二:合并(Python 3.10+)
with (
    open('a.txt') as f1,
    open('b.txt') as f2
):
    data = f1.read() + f2.read()

我个人推荐方式二,代码更扁平,可读性更好。但要注意,Python 3.9及以下版本不支持这种写法,得用contextlib.ExitStack:

from contextlib import ExitStack

with ExitStack() as stack:
    files = [stack.enter_context(open(f'file_{i}.txt')) for i in range(10)]
    # 所有文件都会自动关闭

小技巧:ExitStack特别适合动态数量的资源管理。我在写爬虫时经常用它来管理多个网络连接,优雅又安全。

避坑总结

  • 别吞异常:__exit__返回True会吞掉异常,除非你有特殊需求,否则返回False。
  • 生成器版别忘了try...finally:用@contextmanager时,yield前后的代码必须用try...finally包起来,否则异常时清理逻辑不会执行。
  • 别滥用:上下文管理器适合资源管理,不适合普通逻辑。我见过有人把if...else也写成上下文管理器,那纯粹是炫技,不实用。

好了,上下文管理器就聊到这儿。记住一句话:凡是需要成对操作(开/关、锁/解锁、连接/断开)的地方,都值得考虑用上下文管理器。写代码嘛,省心最重要。