Подтвердить что ты не робот

Недостатки генератора ручек в его потребителе

Это продолжение Обработать исключение, созданное в генераторе, и обсуждает более общую проблему.

У меня есть функция, которая читает данные в разных форматах. Все форматы ориентированы на линию или запись, а для каждого формата имеется специальная функция синтаксического анализа, реализованная в качестве генератора. Таким образом, основная функция чтения получает вход и генератор, который считывает соответствующий формат со входа и возвращает записи к основной функции:

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

Проблема, с которой я сталкиваюсь, заключается в том, что while 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 и сообщения Jon's - очень умный и проницательный материал.

4b9b3361

Ответ 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

Ответ 2

Думая глубже о том, что произойдет в более сложном случае, вид оправдывает выбор Python, избегая пузырьковых исключений из генератора.

Если у меня возникла ошибка ввода-вывода из объекта потока, вероятность просто восстановить и продолжить чтение, если бы структуры, локальные для генератора reset каким-то образом, были бы низкими. Мне нужно было бы смириться с процессом чтения, чтобы продолжить: пропустить мусор, отбросить частичные данные, reset некорректную внутреннюю структуру отслеживания и т.д.

Только для генератора достаточно контекста, чтобы сделать это правильно. Даже если бы вы могли сохранить контекст генератора, имея внешний блок, исключения будут полностью нарушать Закон Деметры. Вся важная информация о том, что окружающий блок должен reset и двигаться дальше, находится в локальных переменных функции генератора! И получение или передача этой информации, хотя это возможно, отвратительна.

Полученное исключение почти всегда будет выбрано после очистки, и в этом случае у читатель-генератора уже будет внутренний блок исключений. Попытка очень тяжело поддерживать эту чистоту в простом случае с мозгом, только чтобы он разбился почти в любом реалистическом контексте, было бы глупо. Так что просто 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.

Ответ 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

Ответ 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 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

Ответ 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))

Ответ 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

Ответ 7

Собственно, генераторы довольно ограничены в нескольких аспектах. Вы нашли одно: сбор исключений не является частью их API.

Вы могли бы взглянуть на вещи Stackless Python, такие как greenlets или сопрограммы, которые предлагают гораздо большую гибкость; но погружение в это немного выходит за рамки.

Ответ 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)

Это не всегда будет лучшим решением, но это, безусловно, вариант, и относительно легко понять.