簡體   English   中英

在使用者中處理生成器異常

[英]Handle generator exceptions in its consumer

這是處理生成器中引發的異常的后續操作,並討論了一個更一般的問題。

我有一個功能,可以讀取不同格式的數據。 所有格式都是面向行或記錄的,每種格式都有一個專用的解析功能,可以作為生成器來實現。 因此,主讀取函數將獲得一個輸入和一個生成器,該生成器將從輸入中讀取其各自的格式並將記錄傳遞回主函數:

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塊中,但是事實證明,這是行不通的。 因此,我必須向parsefunc本身添加try-except ,然后以某種方式將異常傳遞給使用者:

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

我不太願意這樣做,因為

  • 在不打算處理任何異常的函數中使用try毫無意義
  • 我不清楚如何將異常傳遞給使用函數
  • 會有許多格式和許多parsefunc ,我不想用太多的輔助代碼來使它們混亂。

有沒有人建議更好的架構?

致Google員工的注意事項:除了最佳答案外,還請注意senderleJon的帖子-非常聰明和有見地的東西。

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

更深入地思考在更復雜的情況下會發生什么,證明了Python選擇避免從生成器冒泡的異常。

如果我從流對象中收到一個I / O錯誤,那么簡單地能夠恢復並繼續讀取而不用某種方式重置生成器本地結構的幾率將會很小。 我必須以某種方式使自己與閱讀過程保持一致,以便繼續:跳過垃圾,推回部分數據,重置一些不完整的內部跟蹤結構等。

只有生成器具有足夠的上下文來正確執行此操作。 即使您可以保留生成器上下文,讓外部塊處理異常也會完全違反Demeter定律。 周圍塊需要復位並繼續運行的所有重要信息都在生成器功能的局部變量中! 即使有可能,獲取或傳遞這些信息也是令人作嘔的。

產生的異常幾乎總是清理后拋出,在這種情況下,閱讀器生成器將已經具有一個內部異常塊。 試圖在腦部死亡的簡單病例中非常努力地保持這種清潔度,只是讓它在幾乎所有現實情況下都崩潰了,將是很愚蠢的。 因此,只要 try的生成器,你將需要的身體except塊無論如何,在任何復雜的情況下。

但是,如果異常條件看起來像異常,而不像返回值,那將是很好的。 因此,我將添加一個中間適配器以允許這樣做:生成器將產生數據或異常,並且適配器將在適用時重新引發異常。 適配器應該在for循環中稱為first-thing,以便我們可以選擇將其捕獲在循環中並清理以繼續,或者可以中斷循環以捕獲並放棄該過程。 而且,我們應該在安裝程序周圍放置一些la腳的包裝器,以指示即將到來的技巧,並在函數適應時強制調用適配器。

這樣,每一層都將呈現其具有上下文可處理的錯誤,但代價是適配器是有點侵入性的(也許也很容易忘記)。

因此,我們將擁有:

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的調用中。

如果不了解更多有關系統的信息,我認為很難說出哪種方法最有效。 但是,沒有人建議的一種選擇是使用callback 鑒於只有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()訪問traceback信息。

另一種有趣的方法可能是使用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值,重置值,返回上一步等等來做更復雜的事情。 。 你甚至可以等待以基於您會收到錯誤至此發送回調。 但是請注意,一旦生成器yield s就會清除sys.exc_info() ,因此,如果需要訪問回溯,則必須從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

可能的設計示例:

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

我喜歡給定答案的“ 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))

關於將異常從生成器傳播到使用函數的觀點,您可以嘗試使用錯誤代碼(錯誤代碼集)來指示錯誤。 盡管不優雅,但這是您可以想到的一種方法。

例如,在下面的代碼中,產生一個類似於-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

實際上,發電機在幾個方面都非常有限。 您發現了一個:引發異常不是其API的一部分。

您可以看看Greenless或協程這樣的Stackless Python東西,它們提供了更多的靈活性。 但是深入了解這一點超出了范圍。

(我回答了OP中鏈接的其他問題,但我的回答也適用於這種情況)

我已經需要解決幾次這個問題,並在尋找其他人做了什么之后才提出這個問題。

一個選項(可能需要一點重構)可能是簡單地創建一個錯誤處理生成器,然后throw異常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

parsefunc函數提供了一個附加的handler參數,因此它可以放置錯誤:

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)

這並不總是最好的解決方案,但肯定是一種選擇,而且相對容易理解。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM