Python 在其使用者中处理生成器异常

Python 在其使用者中处理生成器异常,python,exception-handling,generator,Python,Exception Handling,Generator,这是对一个更普遍问题的后续讨论 我有一个函数,可以读取不同格式的数据。所有格式都是面向行或记录的,每种格式都有一个专用的解析函数,作为生成器实现。因此,主读取函数获取一个输入和一个生成器,该生成器从输入中读取其各自的格式,并将记录传递回主函数: def read(stream, parsefunc): for record in parsefunc(stream): do_stuff(record) 其中,parsefunc类似于: def parsefunc(stre

这是对一个更普遍问题的后续讨论

我有一个函数,可以读取不同格式的数据。所有格式都是面向行或记录的,每种格式都有一个专用的解析函数,作为生成器实现。因此,主读取函数获取一个输入和一个生成器,该生成器从输入中读取其各自的格式,并将记录传递回主函数:

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,我不想用太多的帮助代码把它们弄得乱七八糟
有人对更好的架构有什么建议吗


谷歌的一个注意事项:除了最重要的答案外,还要注意和发布——非常聪明和有洞察力的东西。

一个可能的设计示例:

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

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

例如,在下面的代码中,生成一个类似于-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

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

如果不了解更多关于系统的信息,我认为很难判断哪种方法最有效。然而,还没有人建议的一个选项是使用回调。考虑到只有
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)
我在一个可变局部上使用了闭包;您还可以定义一个类。还请注意,您可以通过回调中的
sys.exc_info()
访问
回溯
信息

另一个有趣的方法可能是使用
send
。这将有点不同的工作方式;基本上,与定义回调不同,
read
可以检查
yield
的结果,执行许多复杂的逻辑,
发送一个替代值,然后生成器将重新生成该值(或执行其他操作)。这有点异国情调,但我想我会提到它,以防有用:

>>> 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()
在生成器
产生
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

实际上,生成器在几个方面都非常有限。您发现了一个问题:异常的引发不是它们的API的一部分


您可以看看无堆栈Python的东西,比如greenlets或协同程序,它们提供了更大的灵活性;但是,深入研究这一点有点超出了本文的范围。

深入思考在更复杂的情况下会发生什么,可以证明Python避免从生成器中冒泡异常的选择是正确的

如果我从一个流对象得到一个I/O错误,那么在没有以某种方式重置生成器本地结构的情况下,简单地恢复并继续读取的可能性很低。为了继续下去,我不得不让自己与阅读过程保持一致:跳过垃圾、推回部分数据、重置一些不完整的内部跟踪结构,等等

只有生成器有足够的上下文来正确执行该操作。即使您可以保留生成器上下文,让外部块处理异常也将完全无视Demeter定律。周围块需要重置和继续移动的所有重要信息都在生成器函数的局部变量中!获取或传递这些信息,虽然可能,但令人厌恶

产生的异常几乎总是在清理之后抛出,在这种情况下,读取器生成器将已经有一个内部异常块。在头脑死亡的简单情况下,努力保持这种清洁,却在几乎每一个现实环境中都被打破,这是愚蠢的。因此,只要在生成器中使用
try
,在任何复杂的情况下,都需要
的主体,除了

不过,如果异常条件看起来像异常,而不像返回值,那就太好了。因此,我将添加一个中间适配器来实现这一点:生成器将生成数据或异常,并且适配器将重新引发异常(如果适用)。适配器应该首先在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()
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)
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
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):
  ...
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))
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
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()
def read(stream, parsefunc):
    handler = err_handler()
    for record in parsefunc(stream, handler):
        do_stuff(record)