如何在生成器内使用Python上下文管理器
在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
这个小示例在case1中运行得很好,没有抛出异常,但在case2中抛出了属性错误。在这里,我看到抛出了RuntimeException
,因为with语句捕获并忽略了GeneratorExit
异常。
有没有人能帮我理清这个棘手用例的规则?我怀疑是我在__exit__
方法中做错了什么,或者没有做什么。我尝试添加代码来重新抛出GeneratorExit
,但那并没有解决问题。
2 个回答
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__
函数中,你返回了 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"