36 Stimmen

Behandeln Sie Generatorausnahmen in seinem Verbraucher

Dies ist eine Fortsetzung von Behandlung einer in einem Generator geworfenen Ausnahme und behandelt ein allgemeineres Problem.

Ich habe eine Funktion, die Daten in verschiedenen Formaten liest. Alle Formate sind zeilen- oder satzorientiert und für jedes Format gibt es eine dedizierte Parsing-Funktion, die als Generator implementiert ist. Die Hauptlesefunktion erhält also eine Eingabe und einen Generator, der sein jeweiliges Format aus der Eingabe liest und Datensätze an die Hauptfunktion zurückliefert:

def read(stream, parsefunc):
    for record in parsefunc(stream):
        mache_etwas(record)

wobei parsefunc so etwas ist wie:

def parsefunc(stream):
    while not eof(stream):
        rec = read_record(stream)
        mach etwas
        yield rec

Das Problem, dem ich gegenüberstehe, ist, dass obwohl parsefunc eine Ausnahme werfen kann (z.B. beim Lesen aus einem Stream), es keine Ahnung hat, wie damit umzugehen ist. Die Funktion, die für die Behandlung von Ausnahmen verantwortlich ist, ist die Hauptfunktion read. Beachten Sie, dass Ausnahmen auf zeilenweiser Grundlage auftreten, daher sollte der Generator seine Arbeit fortsetzen und Datensätze zurückliefern, auch wenn ein Datensatz fehlschlägt, bis der gesamte Stream erschöpft ist.

In der vorherigen Frage versuchte ich, next(parsefunc) in einen try-Block zu setzen, aber wie sich herausstellte, wird dies nicht funktionieren. Also muss ich ein try-except zur parsefunc selbst hinzufügen und dann irgendwie Ausnahmen an den Verbraucher weitergeben:

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

Ich bin eher zurückhaltend, dies zu tun, weil

  • es keinen Sinn macht, try in einer Funktion zu verwenden, die nicht dazu gedacht ist, Ausnahmen zu behandeln
  • mir unklar ist, wie Ausnahmen an die verbrauchende Funktion übergeben werden sollen
  • es viele Formate und viele parsefunc's geben wird, möchte ich sie nicht mit zu viel Hilfscode überladen.

Hat jemand Vorschläge für eine bessere Architektur?

Eine Notiz für Googler: Neben der besten Antwort beachten Sie auch senderle's und Jon's Beiträge - sehr kluge und aufschlussreiche Sachen.

21voto

Salil Punkte 8969

Sie können ein Tupel aus Datensatz und Ausnahme in der Parsefunktion zurückgeben und die Verbraucherfunktion entscheiden lassen, was mit der Ausnahme zu tun ist:

import random

def get_record(line):
  num = random.randint(0, 3)
  if num == 3:
    raise Exception("3 bedeutet Gefahr")
  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 "Es ist ein Fehler aufgetreten %s" % e
      else:
        print "Datensatz erhalten %s" % rec

14voto

Jon Obermark Punkte 261

Das tiefergehende Überlegen darüber, was in einem komplexeren Fall passieren würde, bestätigt irgendwie die Python-Entscheidung, Ausnahmen nicht aus einem Generator herauszublubbern.

Wenn ich einen E/A-Fehler von einem Stream-Objekt bekommen würde, wären die Chancen, einfach wiederherstellen und mit dem Lesen fortzufahren, ohne dass die Strukturen lokal im Generator auf irgendeine Weise zurückgesetzt würden, gering. Ich müsste mich irgendwie mit dem Lesevorgang abfinden, um fortzufahren: Müll überspringen, teilweise Daten zurückgeben, einige unvollständige interne Verfolgungsstrukturen zurücksetzen usw.

Nur der Generator hat genügend Kontext, um das ordnungsgemäß zu tun. Selbst wenn Sie den Generatorkontext behalten könnten, würde es total gegen das Gesetz von Demeter verstoßen, wenn der äußere Block die Ausnahmen behandeln würde. Alle wichtigen Informationen, die der umgebende Block benötigt, um zurückzusetzen und weiterzumachen, befinden sich in lokalen Variablen der Generatorfunktion! Und diese Informationen zu erhalten oder weiterzugeben, obwohl möglich, ist widerlich.

Die resultierende Ausnahme würde fast immer nach der Bereinigung geworfen, für den Fall, dass der Lese-Generator bereits einen internen Ausnahmeblock hat. Es wäre albern, sich sehr darum zu bemühen, diese Sauberkeit im gehirntoten einfachen Fall aufrechtzuerhalten, nur um festzustellen, dass sie in fast jedem realistischen Kontext zusammenbricht. Also einfach haben Sie das try im Generator, Sie werden den Inhalt des except-Blocks sowieso in jedem komplexen Fall benötigen.

Es wäre schön, wenn Ausnahmebedingungen wie Ausnahmen und nicht wie Rückgabewerte aussehen könnten. Daher würde ich einen Zwischenadapter hinzufügen, um dies zu ermöglichen: Der Generator würde entweder Daten oder Ausnahmen liefern, und der Adapter würde die Ausnahme bei Bedarf erneut auslösen. Der Adapter sollte als Erstes innerhalb der for-Schleife aufgerufen werden, damit wir die Möglichkeit haben, ihn innerhalb der Schleife abzufangen und aufzuräumen, um fortzufahren, oder aus der Schleife auszubrechen, um ihn aufzufangen und den Prozess abzubrechen. Und wir sollten eine Art schwaches Wrapper um das Setup setzen, um anzuzeigen, dass hier Tricks im Gange sind, und um den Adapter zwingen, aufgerufen zu werden, falls die Funktion angepasst wird.

Auf diese Weise werden Fehler jeder Ebene präsentiert, die den Kontext haben, um sie zu behandeln, auf Kosten des Adapters, der ein kleines bisschen aufdringlich ist (und vielleicht auch leicht zu vergessen).

Also hätten wir:

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()

(Wo die beiden try-Blöcke optional sind.)

Der Adapter sieht so aus:

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)

Und parsefunc sieht so aus:

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

Um es schwieriger zu machen, den Adapter zu vergessen, könnten wir auch frozen von einer Funktion zu einem Decorator auf parsefunc ändern.

def frozen_results(func):
  def freezer(__func = func, *args, **kw):
    for item in __func(*args, **kw):
       yield Frozen(item)
  return freezer

In diesem Fall würden wir erklären:

@frozen_results
def parsefunc(stream):
  ...

Und offensichtlich würden wir uns nicht die Mühe machen, frozen zu deklarieren oder ihn um den Aufruf von parsefunc zu wickeln.

7voto

senderle Punkte 135243

Ohne mehr über das System zu wissen, denke ich, dass es schwierig ist zu sagen, welcher Ansatz am besten funktionieren wird. Allerdings könnte eine Option, die noch niemand vorgeschlagen hat, die Verwendung eines Rückrufs sein. Da nur lesen weiß, wie man mit Ausnahmen umgeht, könnte so etwas funktionieren?

def lesen(strom, parsefunc):
    einige_closure_daten = {}

    def fehler_rückruf_1(e):
        manipulieren(some_closure_data, e)
    def fehler_rückruf_2(e):
        transformieren(some_closure_data, e)

    for eintrag in parsefunc(strom, fehler_rückruf_1):
        etwas_machen(eintrag)

Dann, in parsefunc:

def parsefunc(strom, fehler_rückruf):
    while not eof(strom):
        try:
            rec = datensatz_lesen()
            yield rec
        except Exception as e:
            fehler_rückruf(e)

Ich habe hier eine Closure über ein veränderliches lokales verwendet; Sie könnten auch eine Klasse definieren. Beachten Sie auch, dass Sie die traceback-Info über sys.exc_info() im Callback abrufen können.

Ein anderer interessanter Ansatz könnte die Verwendung von send sein. Dies würde ein wenig anders funktionieren; im Grunde könnte lesen anstelle der Definition eines Callbacks das Ergebnis von yield überprüfen, eine Menge komplexer Logik ausführen und einen Ersatzwert senden, den der Generator dann erneut yield (oder etwas anderes damit machen) würde. Dies ist etwas exotischer, aber ich dachte, ich erwähne es, falls es nützlich ist:

>>> def parsefunc(it):
...     standard = None
...     for x in it:
...         try:
...             rec = float(x)
...         except ValueError as e:
...             standard = yield e
...             yield standard
...         else:
...             yield rec
... 
>>> analysierte_werte = parsefunc(['4', '6', '5', '5h', '22', '7'])
>>> for x in analysierte_werte:
...     if isinstance(x, ValueError):
...         x = analysierte_werte.send(0.0)
...     print x
... 
4.0
6.0
5.0
0.0
22.0
7.0

Allein ist dies ein wenig nutzlos ("Warum das Standard direkt von lesen ausgeben?" könnten Sie fragen), aber Sie könnten mit standard im Generator komplexere Dinge tun, Werte zurücksetzen, einen Schritt zurückgehen, und so weiter. Sie könnten sogar darauf warten, dass Sie zu diesem Zeitpunkt basierend auf dem Fehler, den Sie erhalten, einen Rückruf senden. Beachten Sie jedoch, dass sys.exc_info() gelöscht wird, sobald der Generator yield durchführt, also müssen Sie alles von sys.exc_info() senden, wenn Sie Zugriff auf den Traceback benötigen.

Hier ist ein Beispiel, wie Sie die beiden Optionen kombinieren könnten:

import string
zahlen = set(string.digits)

def nur_zahlen(v):
    return ''.join(c for c in v if c in zahlen)

def parsefunc(it):
    standard = None
    for x in it:
        try:
            rec = float(x)
        except ValueError as e:
            callback = yield e
            yield float(callback(x))
        else:
            yield rec

analysierte_werte = parsefunc(['4', '6', '5', '5h', '22', '7'])
for x in analysierte_werte:
    if isinstance(x, ValueError):
        x = analysierte_werte.send(nur_zahlen)
    print x

3voto

Jon Clements Punkte 134241

Ein Beispiel für ein mögliches Design:

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 # nicht returnen, aber könnte etwas benötigen
        # Alle anderen müssen eine Ebene höher gehen - also war es nicht parsbar
        # Wenn es also ein IOError ist, wissen Sie warum, aber das muss abgefangen werden
        # Ausnahmen potenziell, lass einfach die wichtigsten durch

for record in parse_csv(blah):
    print(record)

2voto

Alfe Punkte 51658

Ich mag die gegebene Antwort mit dem Frozen Zeugs. Basierend auf dieser Idee habe ich das hier entwickelt, um zwei Aspekte zu lösen, die mir noch nicht gefallen haben. Der erste waren die Muster, die benötigt wurden, um es niederzuschreiben. Der zweite war der Verlust des Stack-Traces beim Auslösen einer Ausnahme. Ich habe mein Bestes versucht, um das erste Problem zu lösen, indem ich Dekorateure so gut wie möglich verwendet habe. Ich habe versucht, den Stack-Trace zu behalten, indem ich sys.exc_info() anstelle der Ausnahme allein benutzt habe.

Mein Generator würde normalerweise (d.h. ohne meine Änderungen angewendet) so aussehen:

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

Wenn ich es in eine Verwendung einer inneren Funktion umwandeln kann, um den Wert zu bestimmen, der zurückgegeben werden soll, kann ich meine Methode anwenden:

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

Dies ändert noch nichts und das Aufrufen würde einen Fehler mit einem ordentlichen Stack-Trace verursachen:

for e in generator():
  print e

Jetzt, wenn ich meine Dekorateure anwende, würde der Code so aussehen:

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

Optisch ändert sich nicht viel. Und du kannst es immer noch genauso verwenden wie die Version davor:

for e in generator():
  print e

Und du erhältst immer noch einen ordentlichen Stack-Trace beim Aufrufen. (Nur ein Frame mehr ist jetzt dabei.)

Aber jetzt kannst du es auch so nutzen:

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

Auf diese Weise kannst du im Verbraucher jegliche Ausnahme behandeln, die im Generator ausgelöst wurde, ohne zu viel syntaktischen Aufwand und ohne den Verlust des Stack-Traces.

Die Dekorateure sind wie folgt aufgeschlüsselt:

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))

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X