这是对Handle an exception thrown in a generator的后续,并讨论了一个更普遍的问题。
我有一个以不同格式读取数据的函数。所有格式都是面向行或面向记录的,对于每种格式都有一个专用的解析函数,作为生成器实现。因此,主读取函数获得一个输入和一个生成器,它从输入读取其各自的格式,并将记录传递回主函数:
def read(stream, parsefunc):
for record in parsefunc(stream):
do_stuff(record)其中parsefunc是这样的:
def parsefunc(stream):
while not eof(stream):
rec = read_record(stream)
do some stuff
yield rec我面临的问题是,虽然parsefunc可以抛出异常(例如,当从流读取时),但它不知道如何处理它。负责处理异常的函数是主要的read函数。请注意,异常发生在每条记录的基础上,因此即使一条记录失败,生成器也应该继续其工作并生成记录,直到整个流耗尽为止。
在前面的问题中,我试图将next(parsefunc)放在一个try块中,但事实证明,这是行不通的。因此,我必须将try-except添加到parsefunc本身,然后以某种方式将异常传递给消费者:
def parsefunc(stream):
while not eof(stream):
try:
rec = read_record()
yield rec
except Exception as e:
?????我很不愿意这么做,因为
try是没有意义的parsefunc,我不想让它们被太多的辅助代码所干扰。有没有人建议更好的建筑?
谷歌员工注意:除了上面的答案,还要注意senderle's和Jon's的帖子--非常聪明和有洞察力的东西。
发布于 2012-07-07 11:20:14
您可以在parsefunc中返回记录和异常的元组,并让使用者函数决定如何处理异常:
import random
def get_record(line):
num = random.randint(0, 3)
if num == 3:
raise Exception("3 means danger")
return line
def parsefunc(stream):
for line in stream:
try:
rec = get_record(line)
except Exception as e:
yield (None, e)
else:
yield (rec, None)
if __name__ == '__main__':
with open('temp.txt') as f:
for rec, e in parsefunc(f):
if e:
print "Got an exception %s" % e
else:
print "Got a record %s" % rec发布于 2012-09-29 19:57:05
更深入地思考在更复杂的情况下会发生什么,某种程度上证明了Python避免从生成器中冒出异常的选择。
如果我从流对象中得到I/O错误,那么简单地恢复和继续读取的几率就会很低,而不会以某种方式重置生成器的本地结构。为了继续下去,我将不得不与阅读过程保持一致:跳过垃圾,推回部分数据,重置一些不完整的内部跟踪结构,等等。
只有生成器有足够的上下文来正确地执行此操作。即使您可以保持生成器上下文,让外部块处理异常也完全违背了Demeter定律。周围块需要重置和移动的所有重要信息都在生成器函数的局部变量中!而获取或传递这些信息,尽管有可能,却令人恶心。
结果异常几乎总是在清理之后抛出,在这种情况下,读取器生成器将有一个内部异常块。在脑死亡--简单的案例中,努力保持这种清洁状态,但在几乎每一种现实环境中都会出现故障,这将是愚蠢的。因此,只要在生成器中使用try,无论如何,在任何复杂的情况下,您都需要except块的主体。
但是,如果异常条件看起来像异常,而不是返回值,那就太好了。因此,我将添加一个中间适配器来实现这一点:生成器将生成数据或异常,如果适用的话,适配器将重新引发异常。适配器应该在for循环中被称为第一件事,这样我们就可以选择在循环中捕获它并继续清理,或者跳出循环来捕获它并放弃进程。我们应该在设置中放置一些蹩脚的包装器,以指示正在进行的技巧,并在函数适应时强制适配器被调用。
这样,每个层都会出现它需要处理的上下文错误,从而牺牲适配器的微小侵扰性(也许也很容易忘记)。
所以我们会:
def read(stream, parsefunc):
try:
for source in frozen(parsefunc(stream)):
try:
record = source.thaw()
do_stuff(record)
except Exception, e:
log_error(e)
if not is_recoverable(e):
raise
recover()
except Exception, e:
properly_give_up()
wrap_up()(其中两个try块是可选的。)
适配器看起来像:
class Frozen(object):
def __init__(self, item):
self.value = item
def thaw(self):
if isinstance(value, Exception):
raise value
return value
def frozen(generator):
for item in generator:
yield Frozen(item)parsefunc看起来像:
def parsefunc(stream):
while not eof(stream):
try:
rec = read_record(stream)
do_some_stuff()
yield rec
except Exception, e:
properly_skip_record_or_prepare_retry()
yield e为了更难忘记适配器,我们还可以将冻结的函数更改为parsefunc上的装饰器。
def frozen_results(func):
def freezer(__func = func, *args, **kw):
for item in __func(*args, **kw):
yield Frozen(item)
return freezer在这种情况下,我们将宣布:
@frozen_results
def parsefunc(stream):
...显然,我们不会声明frozen,也不会围绕对parsefunc的调用来包装它。
发布于 2012-07-10 17:58:01
如果不了解更多关于系统的信息,我认为很难判断哪种方法最有效。然而,还没有人建议使用回调的一个选项。考虑到只有read知道如何处理异常,这样的事情会有效吗?
def read(stream, parsefunc):
some_closure_data = {}
def error_callback_1(e):
manipulate(some_closure_data, e)
def error_callback_2(e):
transform(some_closure_data, e)
for record in parsefunc(stream, error_callback_1):
do_stuff(record)然后,在parsefunc中
def parsefunc(stream, error_callback):
while not eof(stream):
try:
rec = read_record()
yield rec
except Exception as e:
error_callback(e)我在这里对一个可变的本地使用闭包;您也可以定义一个类。还请注意,您可以通过回调内部的traceback通过sys.exc_info()访问sys.exc_info()信息。
另一种有趣的方法可能是使用send。这可能有点不同;基本上,read不需要定义回调,而是可以检查yield的结果,执行许多复杂的逻辑,并将send作为替代值,然后生成器将重新产生这个值(或者使用其他方法)。这有点异国情调,但我想我应该提一下,以防有用:
>>> def parsefunc(it):
... default = None
... for x in it:
... try:
... rec = float(x)
... except ValueError as e:
... default = yield e
... yield default
... else:
... yield rec
...
>>> parsed_values = parsefunc(['4', '6', '5', '5h', '22', '7'])
>>> for x in parsed_values:
... if isinstance(x, ValueError):
... x = parsed_values.send(0.0)
... print x
...
4.0
6.0
5.0
0.0
22.0
7.0这本身就有点没用了(“为什么不直接从read打印默认值呢?”(您可能会问),但是您可以在生成器中使用default完成更复杂的事情,重置值,返回一个步骤,等等。您甚至可以根据收到的错误,等待在此时发送回调。但是请注意,sys.exc_info()在生成器yield一开始就被清除了,所以如果需要访问回溯,就必须从sys.exc_info()发送所有内容。
下面是一个如何将这两个选项结合在一起的示例:
import string
digits = set(string.digits)
def digits_only(v):
return ''.join(c for c in v if c in digits)
def parsefunc(it):
default = None
for x in it:
try:
rec = float(x)
except ValueError as e:
callback = yield e
yield float(callback(x))
else:
yield rec
parsed_values = parsefunc(['4', '6', '5', '5h', '22', '7'])
for x in parsed_values:
if isinstance(x, ValueError):
x = parsed_values.send(digits_only)
print xhttps://stackoverflow.com/questions/11366892
复制相似问题