Python:用 contextlib.ExitStack 管一组临时资源

53 次浏览2 条回复

脚本里有时会按条件打开好几个文件、临时目录,写一堆嵌套 with 会有点乱。contextlib.ExitStack 适合这种“资源数量运行时才知道”的场景,退出时会按反向顺序统一清理。

环境:Python 3.10+,Linux/macOS/Windows,只用标准库。保存为 exitstack-demo.py:

from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory


def build_files(names):
    with ExitStack() as stack:
        tmp = Path(stack.enter_context(TemporaryDirectory()))
        handles = []

        for name in names:
            path = tmp / f"{name}.txt"
            handle = stack.enter_context(path.open("w+", encoding="utf-8"))
            handle.write(f"hello {name}\n")
            handle.seek(0)
            handles.append(handle)

        for handle in handles:
            print(handle.read().strip())

        print("tmp exists inside:", tmp.exists())
        return tmp


old_tmp = build_files(["api", "worker", "cron"])
print("tmp exists after:", old_tmp.exists())

跑一下:

python3 exitstack-demo.py

输出大概是:

hello api
hello worker
hello cron
tmp exists inside: True
tmp exists after: False

这个比手写 try/finally 的好处是:中间任意一步抛异常,前面已经注册进去的资源也会正常退出。用在“循环里打开多个文件”“临时切换一组上下文”这种小工具里挺顺手。

这个场景还可以顺手提一下 stack.callback(...),不一定非得是 context manager。比如临时改了某个全局开关、加了一个 cleanup 函数,也能注册进去,最后一样按反向顺序跑。

我一般会把 ExitStack 用在两类地方:资源数量是循环里算出来的,或者测试里需要临时拼一组 mock/patch。普通一两个 with 还是直接嵌套更清楚。

散步呢Lv1#1

这个场景还可以顺手提一下 stack.callback(...),不一定非得是 context manager。比如临时改了某个全局开关、加了一个 cleanup 函数,也能注册进去,最后一样按反向顺序跑。

我一般会把 ExitStack 用在两类地方:资源数量是循环里算出来的,或者测试里需要临时拼一组 mock/patch。普通一两个 with 还是直接嵌套更清楚。

还有个小细节:如果中途需要“先攒资源,确认没问题后再把清理责任交出去”,可以用 pop_all()。它会把当前 stack 里的退出回调搬到一个新 stack,原来的 with 退出时就不会清了。

一个能跑的小例子:

from contextlib import ExitStack
from tempfile import TemporaryDirectory
from pathlib import Path

with ExitStack() as stack:
    tmp = Path(stack.enter_context(TemporaryDirectory()))
    keep_cleanup = stack.pop_all()

print(tmp.exists())  # True,还没清理
keep_cleanup.close()
print(tmp.exists())  # False

这个适合初始化阶段要么全部成功、要么全部回滚的代码;不过用了之后一定要保证新 stack 最后会 close(),不然就变成泄漏资源了。