组合Python上下文管理器:一个谜题

3
我对如何安排Python上下文管理器可以做的所有事情感到困惑。
据我所知,构建上下文管理器的潜在元素包括:
A:总是发生的事情 B:C所需的某些准备工作 C:创建和建立在上下文中使用的对象X D:在上下文开始之前使用成功建立的X执行一些事情 E:将X返回到上下文(用于由as使用) F:在上下文结束时使用X进行包装 G:处理进入上下文之前C和B失败的后果 H:处理上下文失败的后果
我想我大致明白每个元素在上下文管理器函数中的位置,但完全不知道如何在类中排列它们。
是否有一个上下文管理器函数和类的模板,显示了这些元素在函数和(特别是)类中的位置?我查看了许多此处和其他地方的示例,但没有找到综合的示例,而且许多示例使用实际代码,我无法总结出每个上述元素的含义。
我认为当通过函数实现时,我基本上理解上下文管理器的行为。
from contextlib import contextmanager     
@contextmanager
def log_file_open(oec_data, build_description, log_dir):
    # A: Something that always happens
    try:
        # B: Some stuff needed to make a_thing
        a_thing = establish_thing_in_a_way_that_might_fail() # C
        # D: Some things that happen using a_thing at context start
        yield a_thing # E
        # F: Wrap up with a_thing when all is well
    except:
        # G: Deal the consequences of failure in try or...
        # H: Deal the consequences of failure in context
    finally:
        # Could F go here instead?

例如,如果要打开一个文件,并在成功打开和关闭时向其写入内容,但如果出现问题,则需要清理该文件,可以编写以下代码:
from contextlib import contextmanager     
@contextmanager
def log_file_open(oec_data, build_description, log_dir):
    print('Entering context...')
    try:
        usable_file_name = get_some_name()
        a_thing =  open(usable_file_name, mode='w')
        a_thing.write('Logging context started.')
        yield a_thing
        a_thing.write('Logging context ended.')
    except:
        a_thing.close()
        os.remove(a_thing.name)
        raise

但我不确定这是否正确,而且我对如何将其映射到类中__enter()____exit()__的使用感到困惑。 它是否(大致)为:

def __init__(self):
    # A: Something that always happens

def __enter__(self):
    try:
        # B: Some stuff needed to make a_thing
        a_thing = establish_thing_in_a_way_that_might_fail() # C
        # D: Some things that happen using a_thing at context start
     except:
        # G: Deal the consequences of failure in try
        a_thing = some_appropriate_blank_value
     finally:
        return a_thing # E

 def __exit__(self, type, value, traceback):
        if type is None:
            # F: Wrap up with a_thing when all is well
            return True
        else:
            # H: Deal the consequences of failure in context
            return False
2个回答

2
您在生成上下文值时混淆了错误处理和上下文本身的错误处理。更好的做法是这样写:
@contextmanager
def fn(...):
    value = ...      # A, B, C, D: setup
    try:
        yield value  # E: pass value to client
    except:          # or better, finally:
        ...          # F, H: cleanup

通过这种方式,您知道您仅处理源自客户端代码的异常,并且随着您知道设置成功,您可以简化清理代码。通常没有必要尝试处理设置代码中的异常; 您不希望客户端代码必须处理 None 上下文值。这意味着 __enter__ 只是:

def __enter__(self):
    self.value = ...   # A, B, C, D: setup
    return self.value  # E: pass value to client

如果 __enter__ 抛出异常,则不会调用 __exit__

还要注意,finallyexcept 更好,除非您计划从客户端代码中禁止异常,这很少有用。因此,__exit__ 简单地是:

def __exit__(self, type, value, traceback):
    ...                # F, H: cleanup
    return False       # don't suppress any exception

2

我认为你的理解大部分是正确的。上下文管理器是一个对象,通过其__enter____exit__方法来管理上下文。因此,在__init__中发生的事情在对象的生命周期内都是真实的。 让我们看一个具体的例子:

class CMan(object):
    def __init__(self, *parameters):
        "Creates a new context manager"
        print "Creating object..."

    def __enter__(self):
        "Enters the manager (opening the file)"
        print "Entering context..."
        a_thing = self # Or any other relevant value to be used in this context
        print "Returning %s" % a_thing
        return a_thing

    def __exit__(self, type, value, traceback):
        "Exits the context"
        if type is None:
            print "Exiting with no exception -> Wrapping up"
            return
        print "Exiting with exception %s" % type

这将用于:

>>> with CMan(1,2,3) as x:
...     print 1 + 1
Creating object...
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up

请注意,即时创建对象并非强制要求:
>>> mgr = CMan(1,2,3)
Creating object...
>>> with mgr as x:
...     print 1 + 1
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up

最后,__exit__ 的返回值决定了是否应该引发异常。如果该值评估为 False(例如:False0None...),则任何异常都将被引发。否则,这意味着上下文管理器已处理了异常,因此不需要引发异常。例如:

>>> class Arithmetic(object):
...     def __enter__(self):
...         return self
...     def __exit__(self, type, value, traceback):
...         if type == ZeroDivisionError:
...             print "I dont care -> Ignoring"
...             return True
...         else:
...             print "Unknown error: Panicking !"
...             return False

>>> with Arithmetic() as a:
...     print 1 / 0 # Divide by 0
I dont care -> Ignoring

>>> with Arithmetic() as a:
...     print 1 + "2" # Type error
Unknown error: Panicking !
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

请注意,在除以0的错误情况下,由于__exit__返回了True,因此错误不会被传播。在其他情况下,在退出上下文管理器后,错误会被引发。你可以将对上下文管理器的调用视为:
>>> with X as x:
...     f(x)

等价于:

>>> x = X.__enter__()
>>> try:
...     exc = None
...     f(x)     
... except Exception as e:
...     exc = e
... finally:
...     handled = X.__exit__(exc)
...     if exc and not handled:
...         raise exc

当然,如果在您的方法__enter____exit__中引发异常,则应适当处理异常,例如,如果生成a_thing可能会失败。您可以通过搜索“Python with语句”在网络上找到许多资源,这通常是您引用此模式的方式(尽管上下文管理器确实更正确)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接