如何在生成器中使用python上下文管理器

时间:2013-03-12 04:16:59

标签: python generator with-statement

在python中,是否应该在生成器中使用with-statements?要清楚,我不是要求使用装饰器从生成器函数创建上下文管理器。我在询问是否存在使用with语句作为生成器内的上下文管理器的固有问题,因为它至少在某些情况下会捕获StopIterationGeneratorExit异常。接下来是两个例子。

Beazley的例子(第106页)提出了一个很好的例子。我已将其修改为使用with语句,以便在opener方法中的yield之后显式关闭文件。我还添加了两种方法,可以在迭代结果时抛出异常。

import os
import fnmatch

def find_files(topdir, pattern):
    for path, dirname, filelist in os.walk(topdir):
        for name in filelist:
            if fnmatch.fnmatch(name, pattern):
                yield os.path.join(path,name)
def opener(filenames):
    f = None
    for name in filenames:
        print "F before open: '%s'" % f
        #f = open(name,'r')
        with open(name,'r') as f:
            print "Fname: %s, F#: %d" % (name, f.fileno())
            yield f
            print "F after yield: '%s'" % f
def cat(filelist):
    for i,f in enumerate(filelist):
        if i ==20:
            # Cause and exception
            f.write('foobar')
        for line in f:
            yield line
def grep(pattern,lines):
    for line in lines:
        if pattern in line:
            yield line

pylogs = find_files("/var/log","*.log*")
files = opener(pylogs)
lines = cat(files)
pylines = grep("python", lines)
i = 0
for line in pylines:
    i +=1
    if i == 10:
        raise RuntimeError("You're hosed!")

print 'Counted %d lines\n' % i

在此示例中,上下文管理器成功关闭了opener函数中的文件。当引发异常时,我看到异常返回的跟踪,但生成器静默停止。如果with语句捕获异常,为什么生成器不会继续?

当我定义自己的上下文管理器以便在生成器中使用时。我收到运行时错误,说我忽略了GeneratorExit。例如:

class CManager(object):  
    def __enter__(self):
          print "  __enter__"
          return self
    def __exit__(self, exctype, value, tb):
        print "  __exit__; excptype: '%s'; value: '%s'" % (exctype, value)
        return True

def foo(n):
    for i in xrange(n):
        with CManager() as cman:
            cman.val = i
            yield cman
# Case1 
for item in foo(10):
    print 'Pass - val: %d' % item.val
# Case2
for item in foo(10):
    print 'Fail - val: %d' % item.val
    item.not_an_attribute

这个小小的demo在case1中工作正常,没有异常引发,但在case2中失败,引发了属性错误。在这里,我看到RuntimeException被引发,因为with语句已经捕获并忽略了GeneratorExit异常。

有人可以帮助澄清这个棘手用例的规则吗?我怀疑这是我正在做的事情,或者不是我的__exit__方法。我尝试添加代码来重新加注GeneratorExit,但这没有帮助。

2 个答案:

答案 0 :(得分:10)

来自Data model entry for object.__exit__

  

如果提供了异常,并且该方法希望抑制异常(即,防止它被传播),则它应该返回一个真值。否则,异常将在退出此方法时正常处理。

__exit__函数中,您将返回True,这将禁止所有例外。如果您将其更改为返回False,则会继续正常引发异常(唯一的区别是您保证调用__exit__函数并且您可以确保在自己之后进行清理)

例如,将代码更改为:

def __exit__(self, exctype, value, tb):
    print "  __exit__; excptype: '%s'; value: '%s'" % (exctype, value)
    if exctype is GeneratorExit:
        return False
    return True

允许您做正确的事情而不是压制GeneratorExit。现在您看到属性错误。也许经验法则应与任何异常处理相同 - 仅拦截异常(如果您知道如何处理它们)__exit__返回True与平均值相比(可能稍微差一点!),除非:

try:
   something()
except: #Uh-Oh
   pass

请注意,当AttributeError被引发(并且未被捕获)时,我认为这会导致生成器对象上的引用计数降至0,然后在生成器中触发GeneratorExit异常,以便它可以清理自己。使用我的__exit__,玩弄以下两种情况,希望你会明白我的意思:

try:
    for item in foo(10):
        print 'Fail - val: %d' % item.val
        item.not_an_attribute
except AttributeError:
    pass

print "Here"  #No reference to the generator left.  
              #Should see __exit__ before "Here"

g = foo(10)
try:
    for item in g:
        print 'Fail - val: %d' % item.val
        item.not_an_attribute
except AttributeError:
    pass

print "Here"
b = g  #keep a reference to prevent the reference counter from cleaning this up.
       #Now we see __exit__ *after* "Here"

答案 1 :(得分:1)

class CManager(object):
    def __enter__(self):
          print "  __enter__"
          return self
    def __exit__(self, exctype, value, tb):
        print "  __exit__; excptype: '%s'; value: '%s'" % (exctype, value)
        if exctype is None:
            return

        # only re-raise if it's *not* the exception that was
        # passed to throw(), because __exit__() must not raise
        # an exception unless __exit__() itself failed.  But throw()
        # has to raise the exception to signal propagation, so this
        # fixes the impedance mismatch between the throw() protocol
        # and the __exit__() protocol.
        #
        if sys.exc_info()[1] is not (value or exctype()):
            raise