如何在上下文管理器中安全地处理异常

How to safely handle an exception inside a context manager

我想我已经读到 with 中的异常不允许 __exit__ 被正确调用。如果我在这张纸条上有误,请原谅我的无知。

所以我这里有一些伪代码,我的目标是使用一个锁上下文,在 __enter__ 上记录开始日期时间和 returns 锁 ID,并在 __exit__ 记录上结束日期时间并释放锁:

def main():
    raise Exception

with cron.lock() as lockid:
    print('Got lock: %i' % lockid)
    main()

除了安全地存在上下文之外,我还能如何引发错误?

注意:我有意在此伪代码中引发基本异常,因为我想在出现任何异常时安全退出,而不仅仅是预期的异常。

注意:Alternative/standard并发预防方法无关紧要,我想将这些知识应用到任何一般的上下文管理中。我不知道不同的上下文是否有不同的怪癖.

PS。 finally 块是否相关?

如果上下文管理器被异常破坏,__exit__方法会正常调用。其实传给__exit__的参数都是为了处理这个case的!来自 the docs:

object.__exit__(self, exc_type, exc_value, traceback)

Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.

If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.

Note that __exit__() methods should not reraise the passed-in exception; this is the caller’s responsibility.

因此您可以看到 __exit__ 方法将被执行,然后,默认情况下,任何异常都将在 退出上下文管理器后重新引发。您可以通过创建一个简单的上下文管理器并用异常打破它来自己测试它:

DummyContextManager(object):
    def __enter__(self):
        print('Entering...')
    def __exit__(self, exc_type, exc_value, traceback):
        print('Exiting...')  
        # If we returned True here, any exception would be suppressed!

with DummyContextManager() as foo:
    raise Exception()

当你 运行 这段代码时,你应该看到你想要的一切(可能是乱序的,因为 print 往往在回溯的中间结束):

Entering...
Exiting...
Traceback (most recent call last):
  File "C:\foo.py", line 8, in <module>
    raise Exception()
Exception

从上面的回答中,我不太清楚使用 @contextlib.contextmanager 时的最佳做法。我关注了link in the comment from @BenUsman.

如果您正在编写上下文管理器,则必须将 yield 包装在 try-finally 块中:

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception