编程Python后端开发工程师

讲讲如何通过contextlib模块中的@contextmanager装饰器使用上下文管理器和资源管理器:它的用途是什么,它是如何工作的,以及有哪些陷阱?

用 Hintsage AI 助手通过面试

回答。

历史 — 在Python中,管理资源(文件、连接、事务)使用基于上下文管理器协议(enter, exit)的with结构。对于简单情况,编写整个类显得冗余,因此提出了@contextmanager装饰器(contextlib模块),允许将资源管理器定义为生成器。

问题 — 手动释放或关闭资源会很麻烦,代码会变得不稳定(例如,忘记关闭文件)。此外,我们不想为了简单的事情(例如,临时更改目录或stdout)编写一个有两个方法的单独类。

解决方案 — 使用@contextmanager简洁地描述资源使用的“开始”和“结束”,保证处理异常和释放资源。

代码示例:

from contextlib import contextmanager @contextmanager def open_file(filename, mode): f = open(filename, mode) try: yield f finally: f.close() with open_file('test.txt', 'w') as f: f.write('Hello')

关键特点:

  • 块的开始 — 所有在yield之前的内容,返回资源。
  • 块的结束 — 在yield之后;必须处理错误并进行关闭/释放。
  • 即使在发生异常时,也保证资源会被关闭。

具有陷阱的问题。

可以让@contextmanager的yield返回多个对象(例如通过元组)吗?

可以,并且很方便返回一组相关的资源。

代码示例:

@contextmanager def managed_two(): a, b = [], {} try: yield a, b finally: a.clear(); b.clear()

如果在yield之后抛出异常,资源会关闭吗?

会,finally块无论如何都会执行,即使在with内部的代码发生错误/异常。

@contextmanager能替代完整的__enter__/__exit__类上下文管理器吗?

在大多数简单情况下 — 可以,但对于更复杂的嵌套状态或继承,使用类会更方便。

常见错误和反模式

  • 跳过finally块,导致在错误时泄漏资源。
  • yield不是唯一的;如果函数中有两个yield,将导致RuntimeError。
  • 尝试将@contextmanager与非生成器函数一起使用(没有yield)。

生活中的示例

负面案例

手动打开和关闭文件:

f = open('test.txt', 'w') try: f.write('Hello') finally: f.close()

优点:

  • 对资源生命周期管理有完全的控制。

缺点:

  • 大量模板代码,容易出错,容易忘记finally。

正面案例

使用@contextmanager临时更改工作目录或打开文件(或环境设置):

@contextmanager def work_in(dirname): import os prev = os.getcwd() os.chdir(dirname) try: yield finally: os.chdir(prev)

优点:

  • 简洁性,保证返回到原始状态。

缺点:

  • 高级抽象可能会掩盖资源管理的细节。