为什么我的上下文管理器函数和类在Python中表现不同?

9 投票
1 回答
2996 浏览
提问于 2025-04-17 19:18

在我的代码中,我需要正确地打开和关闭一个设备,因此我觉得使用上下文管理器是很有必要的。上下文管理器通常是一个包含__enter____exit__方法的类,但似乎也可以通过装饰器来装饰一个函数,使其可以与上下文管理器一起使用(可以参考最近的一篇帖子这里的另一个不错的例子)。

在下面这个(可运行的)代码片段中,我实现了这两种可能性;只需要将注释掉的那一行和另一行互换即可:

import time
import contextlib

def device():
    return 42

@contextlib.contextmanager
def wrap():
    print("open")
    yield device
    print("close")
    return

class Wrap(object):
    def __enter__(self):
        print("open")
        return device
    def __exit__(self, type, value, traceback):
        print("close")


#with wrap() as mydevice:
with Wrap() as mydevice:
    while True:
        time.sleep(1)
        print mydevice()

我尝试运行代码,并用CTRL-C停止它。当我在上下文管理器中使用Wrap类时,__exit__方法会按预期被调用(终端中会打印出'close'),但当我尝试用wrap函数做同样的事情时,终端中却没有打印出'close'。

我的问题是:这个代码片段有什么问题吗?我是不是漏掉了什么,或者为什么用装饰器的函数没有调用print("close")这一行?

1 个回答

17

文档中关于 contextmanager 的例子有点让人误解。函数中在 yield 之后的部分,其实并不完全对应于上下文管理器协议中的 __exit__。文档中的关键点是:

如果在代码块中出现了未处理的异常,它会在生成器中重新抛出,位置正好是在 yield 的地方。因此,你可以使用 try...except...finally 语句来捕获错误(如果有的话),或者确保进行一些清理工作。

所以,如果你想在使用 contextmanager 装饰的函数中处理异常,你需要自己写一个 try,把 yield 包裹起来,并自己处理异常,在 finally 中执行清理代码(或者在 except 中阻止异常,然后在 try/except 之后执行清理)。例如:

@contextlib.contextmanager
def cm():
    print "before"
    exc = None
    try:
        yield
    except Exception, exc:
        print "Exception was caught"
    print "after"
    if exc is not None:
        raise exc

>>> with cm():
...     print "Hi!"
before
Hi!
after

>>> with cm():
...     print "Hi!"
...     1/0
before
Hi!
Exception was caught
after

这个页面也展示了一个很有启发性的例子。

撰写回答