退出python上下文管理器时返回值

Posted

技术标签:

【中文标题】退出python上下文管理器时返回值【英文标题】:Returning value when exiting python context manager 【发布时间】:2019-06-02 06:29:25 【问题描述】:

也许这是一个愚蠢(而且确实不是很实际)的问题,但我之所以问这个问题是因为我无法理解它。

在研究调用上下文管理器中的 return 语句是否会阻止调用 __exit__(不,不会)时,我发现在 __exit__ 和 @ 之间进行类比似乎很常见987654325@ 在 try/finally 块中(例如这里:https://***.com/a/9885287/3471881)因为:

def test():
    try:
        return True
    finally:
        print("Good bye")

将执行相同的操作:

class MyContextManager:

    def __enter__(self):
        return self

    def __exit__(self, *args):
        print('Good bye')

def test():
    with MyContextManager():
        return True

这确实帮助我理解了 cm:s 的工作原理,但在玩了一会儿之后,我意识到如果我们返回一些东西而不是打印,这个类比就行不通了。

def test():
    try:
        return True
    finally:
        return False
test()    
--> False

虽然__exit__ 似乎根本不会回来:

class MyContextManager:

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False

def test():
    with MyContextManager():
        return True

test()
--> True

这让我想到,也许您实际上无法在 __exit__ 中返回任何内容,但您可以:

class MyContextManager:

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return self.last_goodbye()

    def last_goodbye(self):
        print('Good bye')

def test():
    with MyContextManager():
        return True
test()
--> Good bye
--> True

请注意,如果我们在 test() 函数中不返回任何内容,这并不重要。

这引出了我的问题:

是否不可能从 __exit__ 内部返回值,如果是,为什么?

【问题讨论】:

请注意__exit__的返回值有一个定义的含义:如果__exit__返回True,那么在with上下文中抛出的任何异常都将被抑制并且不会传播到外部with 块。 【参考方案1】:

是的。无法从__exit__ 内部更改上下文的返回值。

如果使用return 语句退出上下文,则不能使用context_manager.__exit__ 更改返回值。这与try ... finally ... 子句不同,因为finally 中的代码仍然属于父函数,而context_manager.__exit__ 在自己的范围内运行 .

其实__exit__可以返回一个布尔值(TrueFalse),Python会理解。它告诉 Python 是否应该抑制退出上下文(如果有)的异常(不传播到上下文之外)。

看这个例子__exit__的返回值的含义:

>>> class MyContextManager:
...  def __init__(self, suppress):
...   self.suppress = suppress
...  
...  def __enter__(self):
...   return self
...  
...  def __exit__(self, exc_type, exc_obj, exc_tb):
...   return self.suppress
... 
>>> with MyContextManager(True):  # suppress exception
...  raise ValueError
... 
>>> with MyContextManager(False):  # let exception pass through
...  raise ValueError
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError
>>>

在上面的例子中,ValueErrors 都会导致控件跳出上下文。在第一个块中,上下文管理器的__exit__ 方法返回True,因此Python 抑制了这个异常并且它不会在REPL 中反映。在第二个块中,上下文管理器返回False,因此 Python 让外部代码处理异常,该异常由 REPL 打印出来。

【讨论】:

这里的第一句话我真的没看懂。如果我在 contextManager.__exit__ 内返回一个值,但在 with contextManager(): ... 内不返回 - 我尝试时仍然不会得到返回值。 @user3471881 那是因为__exit__在自己的范围内运行,所以不会影响上下文的返回值。请注意,如果控件到达函数末尾,函数会隐式返回 None【参考方案2】:

解决方法是将结果存储在属性中而不是返回它,然后再访问它。也就是说,如果您打算在不止一次打印中使用该值。

以这个简单的上下文管理器为例:

class time_this_scope():
    """Context manager to measure how much time was spent in the target scope."""

    def __init__(self, allow_print=False):
        self.t0 = None
        self.dt = None
        self.allow_print = allow_print

    def __enter__(self):
        self.t0 = time.perf_counter()

    def __exit__(self, type=None, value=None, traceback=None):
        self.dt = (time.perf_counter() - self.t0) # Store the desired value.
        if self.allow_print is True:
            print(f"Scope took self.dt*1000: 0.1f milliseconds.")

可以这样使用:

with time_this_scope(allow_print=True):
    time.sleep(0.100)

>>> Scope took 100 milliseconds.

或者像这样:

timer = time_this_scope()
with timer:
    time.sleep(0.100)
dt = timer.dt 

不像如下所示,因为随着作用域的结束,timer 对象不再可访问。我们需要修改类as described here,并将return self 的值添加到__enter__。修改前会报错:

with time_this_scope() as timer:
    time.sleep(0.100)
dt = timer.dt 

>>> AttributeError: 'NoneType' object has no attribute 'dt'

最后,这里是一个简单的使用例子:

"""Calculate the average time spent sleeping."""
import numpy as np
import time

N = 100
dt_mean = 0
for n in range(N)
    timer = time_this_scope()
    with timer:
        time.sleep(0.001 + np.random.rand()/1000) # 1-2 ms per loop.
    dt = timer.dt
    dt_mean += dt/N
    print(f"Loop n+1/N took dts.")
print(f"All loops took dt_means on average.)

【讨论】:

以上是关于退出python上下文管理器时返回值的主要内容,如果未能解决你的问题,请参考以下文章

Python学习之旅---上下文管理协议

Python上下文管理器

python魔术方法

python 上下文管理总结

python上下文管理协议,即with的详细使用

python使用上下文管理器实现sqlite3事务机制