处理其消费者中的生成器异常

时间:2012-07-06 17:21:34

标签: python exception-handling generator

这是对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,我不想用太多的帮助代码来混淆它们。

有没有人建议更好的架构?

针对googlers的说明:除了最佳答案之外,请关注senderle'sJon's帖子 - 非常聪明且富有洞察力的内容。

8 个答案:

答案 0 :(得分:16)

您可以在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

答案 1 :(得分:13)

更深入地思考在更复杂的案例中会发生什么,这证明了Python避免将异常冒出生成器的选择。

如果我从流对象中得到I / O错误,那么简单地能够恢复并继续读取的可能性就会很低,而不会使发生器的本地结构以某种方式重置。我会以某种方式与阅读过程保持一致以便继续:跳过垃圾,推回部分数据,重置一些不完整的内部跟踪结构等。

只有生成器有足够的上下文才能正确执行。即使您可以保留生成器上下文,使用外部块处理异常也会完全违反Demeter法则。周围块需要重置和继续运行的所有重要信息都在生成器函数的局部变量中!获取或传递这些信息虽然可能,但令人作呕。

在清理之后几乎总是抛出的结果异常,在这种情况下,reader-generator已经有一个内部异常块。努力在脑死亡的简单情况下保持这种清洁,只是为了让它在几乎每一个现实的背景下都被打破,这将是愚蠢的。所以只要拥有生成器中的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的调用中。

答案 2 :(得分:7)

在不了解系统的情况下,我认为很难说哪种方法最有效。但是,没有人建议的一个选项是使用回调。鉴于只有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()信息。

另一个有趣的方法可能是使用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 x

答案 3 :(得分:3)

可能设计的一个例子:

from StringIO import StringIO
import csv

blah = StringIO('this,is,1\nthis,is\n')

def parse_csv(stream):
    for row in csv.reader(stream):
        try:
            yield int(row[2])
        except (IndexError, ValueError) as e:
            pass # don't yield but might need something
        # All others have to go up a level - so it wasn't parsable
        # So if it's an IOError you know why, but this needs to catch
        # exceptions potentially, just let the major ones propogate

for record in parse_csv(blah):
    print record

答案 4 :(得分:2)

我喜欢Frozen内容的给定答案。基于这个想法,我想出了这个,解决了我还不喜欢的两个方面。第一个是将其写下来所需的模式。第二个是在产生异常时丢失堆栈跟踪。我尽力使用装饰器尽力解决第一个问题。我尝试使用sys.exc_info()而不是单独的异常来保持堆栈跟踪。

我的生成器通常(即没有应用我的东西)看起来像这样:

def generator():
  def f(i):
    return float(i) / (3 - i)
  for i in range(5):
    yield f(i)

如果我可以将其转换为使用内部函数来确定要产生的值,我可以应用我的方法:

def generator():
  def f(i):
    return float(i) / (3 - i)
  for i in range(5):
    def generate():
      return f(i)
    yield generate()

这还没有改变任何东西,并且这样调用它会引发错误并带有正确的堆栈跟踪:

for e in generator():
  print e

现在,应用我的装饰器,代码看起来像这样:

@excepterGenerator
def generator():
  def f(i):
    return float(i) / (3 - i)
  for i in range(5):
    @excepterBlock
    def generate():
      return f(i)
    yield generate()

光学变化不大。你仍然可以像以前那样使用它:

for e in generator():
  print e

调用时你仍然可以获得正确的堆栈跟踪。 (现在只有一帧了。)

但现在你也可以像这样使用它:

it = generator()
while it:
  try:
    for e in it:
      print e
  except Exception as problem:
    print 'exc', problem

通过这种方式,您可以在消费者中处理生成器中引发的任何异常,而不会有太多的语法麻烦并且不会丢失堆栈跟踪。

装饰器拼写如下:

import sys

def excepterBlock(code):
  def wrapper(*args, **kwargs):
    try:
      return (code(*args, **kwargs), None)
    except Exception:
      return (None, sys.exc_info())
  return wrapper

class Excepter(object):
  def __init__(self, generator):
    self.generator = generator
    self.running = True
  def next(self):
    try:
      v, e = self.generator.next()
    except StopIteration:
      self.running = False
      raise
    if e:
      raise e[0], e[1], e[2]
    else:
      return v
  def __iter__(self):
    return self
  def __nonzero__(self):
    return self.running

def excepterGenerator(generator):
  return lambda *args, **kwargs: Excepter(generator(*args, **kwargs))

答案 5 :(得分:1)

关于从生成器到消费函数传播异常的点, 您可以尝试使用错误代码(错误代码集)来指示错误。 虽然不是优雅,但你可以想到一种方法。

例如,在下面的代码中产生一个像你期望的值-1的值 一组正整数将向调用函数发出信号 错误。

In [1]: def f():
  ...:     yield 1
  ...:     try:
  ...:         2/0
  ...:     except ZeroDivisionError,e:
  ...:         yield -1
  ...:     yield 3
  ...:     


In [2]: g = f()

In [3]: next(g)
Out[3]: 1

In [4]: next(g)
Out[4]: -1

In [5]: next(g)
Out[5]: 3

答案 6 :(得分:1)

实际上,发电机在几个方面非常有限。您找到了一个:异常的提升不是其API的一部分。

你可以看一下像greenlet或coroutines这样提供更多灵活性的Stackless Python。但潜入这一点有点超出范围。

答案 7 :(得分:1)

(我回答了OP中关联的另一个问题,但我的回答也适用于这种情况)

我需要解决这个问题几次,并在搜索了其他人做过的事后发现了这个问题。

一个选项 - 可能需要稍微重构 - 将简单地创建一个错误处理生成器,并throw生成器中的异常(到另一个错误处理生成器)而不是{{1}它。

以下是错误处理生成器函数的外观:

raise

def err_handler(): # a generator for processing errors while True: try: # errors are thrown to this point in function yield except Exception1: handle_exc1() except Exception2: handle_exc2() except Exception3: handle_exc3() except Exception: raise 函数提供了一个额外的handler参数,因此它可以放置错误:

parsefunc

现在只使用原始的def parsefunc(stream, handler): # the handler argument fixes errors/problems separately while not eof(stream): try: rec = read_record(stream) do some stuff yield rec except Exception as e: handler.throw(e) handler.close() 函数,但现在使用错误处理程序:

read

这并不总是最好的解决方案,但它肯定是一种选择,而且相对容易理解。