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

22
在Python中,生成器内应该使用with语句吗?需要明确的是,我不是在问是否可以使用装饰器从生成器函数创建上下文管理器。我的问题是,在生成器内使用with语句作为上下文管理器是否存在固有问题,因为它将至少在某些情况下捕获StopIteration和GeneratorExit异常。以下是两个示例。
其中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

在第一种情况下,这个小演示很好地运行而没有引发任何异常,但是在第二种情况下失败了,其中引发了一个属性错误。我看到一个 RuntimeException 被提出,因为 with 语句已经捕获并忽略了一个 GeneratorExit 异常。

有人能帮忙澄清这个棘手的用例的规则吗?我怀疑是我在我的 __exit__ 方法中做或没做某些事情。我尝试添加代码重新引发 GeneratorExit ,但没有帮助。

2个回答

10

对象模型中的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。现在你只会看到属性错误。也许经验法则应该与任何异常处理相同--仅在您知道如何处理它们时拦截异常。具有返回True__exit__与仅仅使用except相比几乎一样糟糕。

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"

@mgilson 感谢您的出色回答。 似乎生成器函数并不是捕获属性错误的方法。这正是我想要的行为,但似乎不可能实现。我希望使用一个with语句来集中处理一系列生成器中的异常。 - David
@David -- 上下文管理器是我最近一直在使用的东西,所以它们仍然在我脑海中 :). 这是一个很好的问题。我希望所有新用户都能问出这样好的第一个问题 :). - mgilson
@lukecampbell -- 很高兴你喜欢它 :). 这确实是一个有趣的问题。 - mgilson
@mgilson 在 Beazley 和 StackOverflow 的帮助下,我已经花了三年的时间寻找现有的答案,而从未需要提出问题。 - David
With语句非常好用 - 即使它们无法适应这种用例。去年夏天,我与@lukecampbell一起使用with语句和redis协调分布式进程:gist - David
@David -- 非常棒:)。 我问过几个问题。有时候我会有一个问题,谷歌搜索也不能找到答案。即使我有解决问题的想法,我有时也会提出问题,希望对别人有所帮助。 - mgilson

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 

文档说明 __exit__ 不应该重新引发传入的异常... - mgilson
@mgilson,奇怪...我从contextlib.py中借鉴了这个想法。 - John La Rooy
说实话,我真的不知道为什么你不应该...虽然我也不知道为什么你想要这样做。如果返回一个“False-y”值,异常仍然会继续传播。 - mgilson
@mgilson,我添加了相关的解释。 - John La Rooy

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