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

22 投票
2 回答
10528 浏览
提问于 2025-04-17 18:46

在Python中,生成器里面应该使用with语句吗?我想说的是,我并不是在问如何用装饰器把生成器函数变成上下文管理器。我是在问,在生成器里使用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

这个小示例在case1中运行得很好,没有抛出异常,但在case2中抛出了属性错误。在这里,我看到抛出了RuntimeException,因为with语句捕获并忽略了GeneratorExit异常。

有没有人能帮我理清这个棘手用例的规则?我怀疑是我在__exit__方法中做错了什么,或者没有做什么。我尝试添加代码来重新抛出GeneratorExit,但那并没有解决问题。

2 个回答

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 

当然可以!请把你想要翻译的内容发给我,我会帮你把它变得更简单易懂。

10

来自对象的 __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 的做法和直接使用 bare 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"

撰写回答