问题 在Python中编写一个上下文管理器,它本身使用with语句


我正在尝试编写一个使用其他上下文管理器的上下文管理器,因此客户端不需要知道整个配方,只需要知道我正在呈现的界面。我无法使用它 @contextmanager  - 之后的代码 yield 如果你被异常打断,调用不会被执行,所以我需要使用基于类的管理器。

这是一个小例子脚本:

from contextlib import contextmanager
import pprint

d = {}

@contextmanager
def simple(arg, val):
    print "enter", arg
    d[arg] = val
    yield
    print "exit", arg
    del d[arg]

class compl(object):
    def __init__(self, arg, val):
        self.arg=arg
        self.val=val

    def __enter__(self):
        with simple("one",1):
            with simple("two",2):
                print "enter complex", self.arg
                d[self.arg] = self.val

    def __exit__(self,*args):
        print "exit complex", self.arg
        del d[self.arg]

print "before"
print d
print ""

with compl("three",3):
    print d
    print ""

print "after"
print d
print ""

这输出:

before
{}

enter one
enter two
enter complex three
exit two
exit one
{'three': 3}

exit complex three
after
{}

我希望它输出这个:

before
{}

enter one
enter two
enter complex three
{'one': 1, 'three': 3, 'two': 2}

exit complex three
exit two
exit one
after
{}

有没有办法告诉基于类的上下文管理器将自己与其他上下文管理器包装在一起?


959
2017-07-11 20:34


起源

指定Python版本会很有用。 - Jon Clements♦
请原谅这个问题,但你为什么要这样做呢?对我来说,基于类的上下文管理器在清除其依赖关系之后应该最后退出似乎是很自然的。 - Fred Foo
瞄准python 2.7,抱歉 - Andrew Roberts
我的用例是用于测试视图的登录上下文管理器(在webapp中)。登录需要几次调用mock,这些调用通过with语句进行。我希望这个帮助程序logIn上下文管理器的用户能够调用登录管理器,而不必知道要模拟的内容。外部CM需要它们的上下文通过传递给登录CM的块来持久化,因此它们不能在登录的enter方法中到期。 - Andrew Roberts


答案:


@contextmanager
def compl(arg, val):
    with simple("one",1):
        with simple("two",2):
            print "enter complex", arg 
            try:
                d[arg] = val
                yield
            finally:
                del d[arg]
                print "exit complex", arg

12
2017-07-11 20:58



你能找出/解释提问者代码中的问题是什么,所以理解正在发生的事情会更快吗? :) - n611x007
@naxa:看一下问题中的最后两个输出示例。问题中的代码产生第一个输出,我在答案中的代码产生第二个输出(理想的一个)。简而言之:最嵌套的上下文管理器应该尽快退出。 - jfs


你写道,“我不能用@contextmanager来做 - 如果你被异常打断,那么在yield调用之后的代码就不会被执行。”如果你有必须运行的代码,你可以把它放在一个 try/finally 块。

import contextlib

@contextlib.contextmanager
def internal_cm():
    try:
        print "Entering internal_cm"
        yield None
        print "Exiting cleanly from internal_cm"
    finally:
        print "Finally internal_cm"

@contextlib.contextmanager
def external_cm():
    with internal_cm() as c:
        try:
            print "In external_cm_f"
            yield [c]
            print "Exiting cleanly from external_cm_f"
        finally:
            print "Finally external_cm_f"

if "__main__" == __name__:
    with external_cm() as foo1:
        print "Location A"
    print
    with external_cm() as foo2:
        print "Location B"
        raise Exception("Some exception occurs!!")

输出:

Entering internal_cm
In external_cm_f
Location A
Exiting cleanly from external_cm_f
Finally external_cm_f
Exiting cleanly from internal_cm
Finally internal_cm

Entering internal_cm
In external_cm_f
Location B
Finally external_cm_f
Finally internal_cm
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Anaconda\lib\site-packages\spyderlib\widgets\externalshell\sitecustomize.py", line 540, in runfile
    execfile(filename, namespace)
  File "C:\untitled0.py", line 35, in <module>
    raise Exception("Some exception occurs!!")
Exception: Some exception occurs!!

2
2018-02-09 00:51





你正在做的事情的麻烦在于使用 with 在你的 __enter__ 调用,当您进入包装上下文管理器时,您既可以进入,也可以离开包装的上下文管理器。如果你想编写自己的上下文管理器,当你进入包装器进入包装的上下文管理器,然后在你离开时退出它们,你将不得不手动调用包装的上下文管理器的功能。您可能还需要担心异常安全问题。


1
2017-07-11 20:59