当前位置: 代码网 > it编程>前端脚本>Python > 深入浅出Python contextlib如何优雅管理上下文资源

深入浅出Python contextlib如何优雅管理上下文资源

2026年04月03日 Python 我要评论
​凌晨三点,小陈盯着屏幕上的报错信息,头皮发麻。“resourcewarning: unclosed file”就这一行警告,让他在一堆历史代码里翻了两个小时。打开的文件忘记关

凌晨三点,小陈盯着屏幕上的报错信息,头皮发麻。

“resourcewarning: unclosed file”

就这一行警告,让他在一堆历史代码里翻了两个小时。打开的文件忘记关了,数据库连接没释放,临时修改的目录路径也没改回来。代码跑起来没问题,但跑久了服务器就开始报“too many open files”。

同事老张路过,瞥了一眼屏幕:“你还在手动写 try...finally 呢?用 contextlib 啊,几行装饰器的事。”

小陈一脸懵:“那是什么?”

这就是很多 python 开发者都会经历的阶段——被资源管理问题折磨过后,才发现标准库里藏着一个宝藏模块。

一、从一个没人关的文件说起

先看一段最常见的代码:

f = open('data.txt', 'r')
data = f.read()
print(data)
f.close()

写过 python 的人都知道,这样写不够安全。如果 f.read() 中间抛异常,f.close() 永远不会执行,文件句柄就泄露了。

于是大家学会了 with 语句:

with open('data.txt', 'r') as f:
    data = f.read()
    print(data)

离开 with 代码块,文件自动关闭,不管里面有没有报错。

这个 with 语句背后的原理,就是上下文管理器。一个类只要实现了 __enter____exit__ 两个魔法方法,就能放进 with 里用。

但问题来了:每次都要写一个完整的类,就为了管理一个资源?太啰嗦了。

contextlib 就是来解决这个痛点的。

二、contextlib 的核心武器:@contextmanager 装饰器

@contextmanager 是 contextlib 模块里最常用、也最好用的工具。它能把一个普通的生成器函数直接变成上下文管理器。

看看怎么用:

from contextlib import contextmanager

@contextmanager
def managed_file(filename):
    f = open(filename, 'r')
    try:
        yield f
    finally:
        f.close()

# 使用方式
with managed_file('data.txt') as f:
    data = f.read()
    print(data)

关键点在这里: yield 前面的代码相当于 __enter__yield 后面的代码(放在 finally 里)相当于 __exit__。不管 with 代码块里发生什么,finally 都会执行,文件一定能关掉。

这个写法比写一个完整的类清爽太多了。几行代码搞定,逻辑一目了然。

三、一个真实的数据库连接场景

假设你在写一个 web 爬虫,需要把数据存到 sqlite 数据库。每次操作都要打开连接、获取游标、提交事务、关闭连接,写起来非常繁琐:

def save_data(data):
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    cursor.execute('insert into items values (?)', (data,))
    conn.commit()
    conn.close()

但这样写有几个隐患:如果 execute 报错,conn.commit()conn.close() 都不会执行,数据库连接就悬在那里了。

@contextmanager 包装一下:

@contextmanager
def get_db():
    conn = sqlite3.connect('app.db')
    try:
        yield conn
        conn.commit()
    except exception:
        conn.rollback()
        raise
    finally:
        conn.close()

# 使用
with get_db() as conn:
    cursor = conn.cursor()
    cursor.execute('insert into items values (?)', ('test',))

现在不管代码执行成功还是报错,连接都会正确关闭。成功了自动提交,失败了自动回滚。

一个装饰器,把资源管理的复杂度全部封装掉了。

四、计时器:用上下文做性能监控

上下文管理器不只能管理“打开-关闭”类的资源,任何“进来时做一件事、出去时做另一件事”的场景都能用。

比如你想测一段代码的执行时间:

import time
from contextlib import contextmanager

@contextmanager
def timer(name):
    start = time.time()
    print(f"{name} 开始...")
    yield
    elapsed = time.time() - start
    print(f"{name} 完成,耗时 {elapsed:.2f} 秒")

# 使用
with timer("数据清洗"):
    # 这里放你要测的代码
    data = [i**2 for i in range(1000000)]
    print(f"生成了 {len(data)} 条数据")

输出:

数据清洗 开始...
生成了 1000000 条数据
数据清洗 完成,耗时 0.18 秒

不需要写一堆 start = time.time()print(...) 的重复代码。把计时逻辑包进上下文管理器里,用的时候一行 with timer(...) 就搞定了。

这对性能调优特别有用。 你可以快速给多个代码块加上计时,找出瓶颈在哪里。

五、临时切换目录:用完自动恢复

写脚本的时候,经常需要临时切换工作目录去处理文件。处理完得切回来,不然会影响后面的代码。

手动写 os.chdir 很容易忘记切回来:

import os

os.chdir('/tmp')
# 处理临时文件...
# 糟糕,忘记切回原来的目录了
os.remove('important.txt')  # 删错了!

用 contextlib 包装一下:

@contextmanager
def cd(path):
    old_dir = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(old_dir)

# 使用
with cd('/tmp'):
    # 在 /tmp 目录下操作
    with open('temp.txt', 'w') as f:
        f.write('临时数据')
    # 离开 with 块自动切回原目录

# 这里已经回到原来的目录了

这个模式可以应用到很多场景:临时修改环境变量、临时重定向标准输出、临时禁用信号处理……核心思路都一样:进去时保存状态,出来时恢复状态。

六、更高级的工具:exitstack

有时候你需要同时管理多个资源,而且这些资源的数量在运行时才能确定。比如打开一批文件:

files = []
for filename in file_list:
    files.append(open(filename, 'r'))
# 万一中间某个文件打开失败,前面已经打开的文件怎么关?

手动处理会非常麻烦。exitstack 就是为这种场景设计的:

from contextlib import exitstack

with exitstack() as stack:
    files = []
    for filename in file_list:
        f = stack.enter_context(open(filename, 'r'))
        files.append(f)
    # 所有文件都成功打开,继续处理
    for f in files:
        print(f.read())
# 离开 with 块时,所有文件按相反顺序自动关闭

exitstack 内部维护了一个栈。每次调用 enter_context,它就把这个资源记下来。离开 with 块时,按照后进先出的顺序自动清理所有资源。不管中间哪个步骤出问题,已经成功打开的资源都会被正确关闭。

还有一个常用场景:在旧代码里,有些资源不是上下文管理器,只有 close 方法。exitstack 也能处理:

with exitstack() as stack:
    conn = stack.callback(lambda: db.close())
    # 离开时 db.close() 会被自动调用

callback 方法让你可以注册任意的清理函数,非常灵活。

七、suppress:忽略你不关心的异常

有时候你并不想处理某个异常,只想让它静悄悄地过去。

比如删除一个可能不存在的文件:

try:
    os.remove('temp.txt')
except filenotfounderror:
    pass

try-except-pass 太啰嗦了。contextlib.suppress 专门解决这个问题:

from contextlib import suppress

with suppress(filenotfounderror):
    os.remove('temp.txt')

可以同时抑制多种异常:

代码干净了很多,意图也很明确:“这个异常出现了也没关系,忽略它。”

八、nullcontext:需要但不需要管理的时候

写函数的时候,有时候需要根据参数决定是否使用上下文管理器。比如调试模式下打开日志文件,生产模式下什么都不做:

def process_data(debug=false):
    if debug:
        manager = open('debug.log', 'w')
    else:
        manager = ???  # 这里放什么?

    with manager as log:
        log.write('处理中...')

nullcontext 就是那个“什么都不做”的上下文管理器:

from contextlib import nullcontext

def process_data(debug=false):
    if debug:
        manager = open('debug.log', 'w')
    else:
        manager = nullcontext()

    with manager as log:
        # 如果 debug=true,log 是文件对象
        # 如果 debug=false,log 是 none,但代码仍然可以正常执行
        if log is not none:
            log.write('处理中...')
        # 实际业务逻辑
        print("数据处理完成")

这样你就不需要写两套逻辑,一套带 with 一套不带。统一用 with 结构,nullcontext 会乖乖地什么也不做。

九、把多个上下文管理器串起来

python 3.10 之后,contextlib 提供了一个更简洁的写法。以前你要嵌套多个 with

with open('input.txt') as infile:
    with open('output.txt', 'w') as outfile:
        outfile.write(infile.read())

现在可以写成一行:

with (
    open('input.txt') as infile,
    open('output.txt', 'w') as outfile
):
    outfile.write(infile.read())

括号把多个上下文管理器包在一起,python 会自动按顺序进入、按相反顺序退出。代码层级少了一层,看起来舒服很多。

十、实战:封装一个重试机制

把这些技巧组合起来,能做出很实用的工具。比如一个带重试功能的上下文管理器:

import time
from contextlib import contextmanager

@contextmanager
def retry(max_attempts=3, delay=1):
    last_exception = none
    for attempt in range(max_attempts):
        try:
            yield
            return  # 成功就退出
        except exception as e:
            last_exception = e
            print(f"第 {attempt + 1} 次尝试失败: {e}")
            if attempt < max_attempts - 1:
                time.sleep(delay)
    raise last_exception

# 使用
with retry(max_attempts=5, delay=2):
    # 这里放可能会临时失败的代码
    response = requests.get('https://unstable-api.example.com/data')
    response.raise_for_status()

这个上下文管理器会自动重试 5 次,每次失败等 2 秒。如果 5 次都失败,抛出最后一次的异常。

调用方的代码非常干净,不需要写任何重试逻辑。这就是 contextlib 的魅力——把横切关注点(cross-cutting concerns)封装起来,让业务代码保持简洁。

十一、contextlib 的底层原理

@contextmanager 装饰一个生成器函数,python 背后做了这些事:

  • 调用函数,得到一个生成器对象
  • 调用生成器的 __next__(),执行到 yield 暂停,yield 的值作为 __enter__ 的返回值
  • 执行 with 代码块
  • 代码块正常结束或抛出异常时,调用生成器的 throw()__next__(),执行 yield 后面的代码
  • 生成器执行结束

所以 yield 后面的代码一定要放在 try-finally 里,确保不管 with 块里发生了什么,清理逻辑都能执行。

这个设计非常巧妙。 生成器本来是用来产生序列的,python 把它复用到上下文管理器的场景,用 yield 切开了“进入”和“退出”两个阶段。

写在最后

回头再看小陈的故事。如果当时他知道 @contextmanager,打开数据库连接的那段代码会写成这样:

@contextmanager
def get_conn():
    conn = create_conn()
    try:
        yield conn
    finally:
        conn.close()

三个函数调用,一个 yield,一个 finally,搞定。不用写类,不用记 __enter____exit__,代码意图清晰得不能再清晰。

contextlib 这个模块不大,但每一行代码都经过精心设计。 它解决的是一个很具体的问题——让 with 语句的编写变得简单。但因为这个“具体问题”在编程里几乎天天遇到,它的价值就被无限放大了。

下次你再遇到需要“进去时做点事、出来时做点事”的场景,先想想能不能用 @contextmanager 包装一下。写出一个漂亮的上下文管理器,那种“用起来真舒服”的感觉,比写一百行注释都来得实在。

彩蛋: contextlib 还有 abstractcontextmanagerasynccontextdecorator 等高级工具,适合在写框架或库的时候使用。不过对于 90% 的日常开发,@contextmanager 加上 exitstacksuppressnullcontext 这四个工具,已经足够应对绝大多数资源管理问题了。

以上就是深入浅出python contextlib如何优雅管理上下文资源的详细内容,更多关于python contextlib管理上下文的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com