36

これは、ジェネレーターでスローされた例外を処理するためのフォローアップであり、より一般的な問題について説明します。

さまざまな形式のデータを読み取る機能があります。すべてのフォーマットは行指向またはレコード指向であり、各フォーマットには、ジェネレーターとして実装された専用の解析機能があります。したがって、メインの読み取り関数は入力とジェネレーターを取得します。ジェネレーターは、入力からそれぞれの形式を読み取り、レコードをメイン関数に返します。

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関数です。例外はレコードごとに発生するため、1つのレコードに障害が発生した場合でも、ジェネレータは作業を続行し、ストリーム全体が使い果たされるまでレコードを返す必要があることに注意してください。

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の'がありますが、あまりにも多くのヘルパーコードでそれらを乱雑にしたくありません。

より良いアーキテクチャについて誰か提案がありますか?

グーグルへのメモ:トップアンサーに加えて、 senderleJonの投稿に注意を払ってください-非常に賢くて洞察に満ちたものです。

4

8 に答える 8

20

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
于 2012-07-07T11:20:14.223 に答える
14

より複雑なケースで何が起こるかをより深く考えると、ジェネレーターからの例外のバブリングを回避するという 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()

(2 つの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

アダプターを忘れにくくするために、frozen を関数から 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

于 2012-09-29T19:57:05.287 に答える
7

システムについて詳しく知らなければ、どのアプローチが最も効果的かを判断するのは難しいと思います。ただし、まだ誰も提案していない1つのオプションは、コールバックを使用することです。例外を処理する方法しか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()

もう1つの興味深いアプローチは、を使用することです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時点でコールバックの送信を待つこともできます。ただし、ジェネレータがsになるとすぐにクリアされるため、トレースバックにアクセスする必要がある場合は、からすべてを送信する必要があることに注意してください。sys.exc_info()yieldsys.exc_info()

2つのオプションを組み合わせる方法の例を次に示します。

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
于 2012-07-10T17:58:01.707 に答える
3

可能な設計の例:

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
于 2012-07-06T18:06:01.203 に答える
2

私はそのようなもので与えられた答えが好きFrozenです。そのアイデアに基づいて、私がまだ好きではなかった 2 つの側面を解決して、これを思いつきました。最初は、それを書き留めるために必要なパターンでした。2 つ目は、例外が発生したときにスタック トレースが失われたことです。できるだけ良いデコレータを使用して、最初の問題を解決するために最善を尽くしました。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

また、呼び出し時に適切なスタック トレースを取得できます。(あと 1 フレームしかありません。)

しかし、次のように使用することもできます。

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))
于 2013-02-28T16:02:29.130 に答える
1

実際、ジェネレーターはいくつかの面でかなり制限されています。あなたは1つを見つけました:例外の発生は彼らのAPIの一部ではありません。

より多くの柔軟性を提供するグリーンレットやコルーチンのようなStacklessPythonのものを見ることができます。しかし、それに飛び込むことはここでは少し範囲外です。

于 2012-09-05T10:54:51.867 に答える
1

ジェネレーターから消費関数に例外を伝播するポイントについては、エラー コード (エラー コードのセット) を使用してエラーを示すことができます。エレガントではありませんが、これは考えられる 1 つのアプローチです。

たとえば、以下のコードでは、-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
于 2012-07-06T19:20:47.383 に答える