在其消费手柄产生异常在其消费手柄产生异常(Handle generator exceptions i

2019-06-14 16:34发布

这是一个后续处理在发电机抛出的异常 ,并讨论了一个更一般的问题。

我有一个功能,以不同的格式读取数据。 所有格式都是线状或面向记录和每种格式有一个专门的解析功能,作为发电机来实现。 所以主要的阅读功能得到输入和发电机,从输入读取相应的格式,并提供记录回到主功能:

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-exceptparsefunc自身,然后以某种方式提供例外的消费者:

def parsefunc(stream):
    while not eof(stream):
        try:
            rec = read_record()
            yield rec
        except Exception as e:
            ?????

我相当不愿意这样做,因为

  • 这是没有意义的使用try在不打算处理任何异常的函数
  • 这是我不清楚如何异常传递到消费功能
  • 有将是多种格式和许多parsefunc的,我不希望有太多的帮手代码杂乱他们。

有什么更好的架构任何人的建议?

让Google的注意事项:除了上面的答案,注意senderle的和乔恩的职位-非常聪明和有见地的东西。

Answer 1:

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


Answer 2:

考虑在更复杂的情况下会发生什么更深层次的一种维护了Python的选择,避免起泡异常出发电机的。

如果我从一个流中的I / O错误对象只是能恢复和继续读书,没有本地的发电机以某种方式被重置结构的可能性,会很低。 我会以某种方式与阅读过程中调和自己才能继续:跳过垃圾,推背的部分数据,重新设定不完整的内部跟踪结构,等等。

只有发电机有足够的背景下这样做正确。 即使你能保持发电机背景下,具有外部块处理异常会完全无视迪米特法则。 所有这些周边块需要重置,继续前进的重要信息在生成函数的局部变量! 并获得或传递的信息,虽然可能是恶心。

由此导致的异常几乎总是清理,在这种情况下,阅读器,发电机将已经有一个内部异常块之后被抛出。 拼命保持这种洁净度脑死简单情况下,只把它分解几乎在每一个真实的环境将是愚蠢的。 因此,只要 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



Answer 3:

不知道更多有关系统,我认为这是很难说什么样的方法将是最好的。 然而,没有人建议尚未一种选择是使用一个回调 。 鉴于只有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 S,所以你必须从发送一切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


Answer 4:

一个可能的设计的一个示例:

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


Answer 5:

我喜欢与给定答案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))


Answer 6:

关于您从发电机传播例外消费函数的时候,你可以尝试使用一个错误代码(一套错误代码)以指示错误。 虽然不优雅那是你能想到的一个方法。

例如,在下面的代码产生像-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


Answer 7:

实际上,发电机都相当几个方面的限制。 你找到一个:异常的提高不是他们的API的一部分。

你可以看看在无堆栈的Python的东西一样greenlets或提供了更大的灵活性协同程序; 但潜入就是有点超出范围在这里。



Answer 8:

(我回答在OP链接的其他问题,但我的答案适用于这种情况以及)

我有需要解决此问题几次和别人做了搜索后,在这个问题就来了。

这可能需要重构事情有点比特一个选项-将简单地创建一个错误处理生成,并throw异常的发电机(另一个错误处理生成器),而不是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功能,但现在有一个错误处理程序:

def read(stream, parsefunc):
    handler = err_handler()
    for record in parsefunc(stream, handler):
        do_stuff(record)

这不是总是将是最好的解决办法,但它肯定是一个选项,比较容易理解。



文章来源: Handle generator exceptions in its consumer