【问题标题】:Putting together a Python context manager: a puzzle组装一个 Python 上下文管理器:一个难题
【发布时间】:2014-01-24 01:11:41
【问题描述】:

我很困惑如何将 Python 上下文管理器可以做的所有事情安排到适当的位置。

据我了解,可能用于构建上下文管理器的元素包括:

  • 答:总会发生的事情
  • 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

【问题讨论】:

    标签: python try-catch with-statement contextmanager


    【解决方案1】:

    您在生成上下文值时混淆了错误处理和上下文本身中的错误处理。写起来要好得多:

    @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 statement'在网上找到很多资源,这通常是你引用这种模式的方式(虽然上下文管理器确实更正确)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-02-05
        • 1970-01-01
        • 2021-10-22
        • 2014-10-20
        • 2011-03-02
        • 2018-09-15
        相关资源
        最近更新 更多