web-dev-qa-db-ja.com

コンテキストマネージャー内の例外の処理

リソースに到達しようとするコードがありますが、使用できない場合があり、例外が発生します。 context managersを使用して再試行エンジンを実装しようとしましたが、コンテキストマネージャーの__enter__コンテキスト内の呼び出し元によって発生した例外を処理できません。

class retry(object):
    def __init__(self, retries=0):
        self.retries = retries
        self.attempts = 0
    def __enter__(self):
        for _ in range(self.retries):
            try:
                self.attempts += 1
                return self
            except Exception as e:
                err = e
    def __exit__(self, exc_type, exc_val, traceback):
        print 'Attempts', self.attempts

これらは、例外を発生させるいくつかの例です(私は処理することを期待していました)。

>>> with retry(retries=3):
...     print ok
... 
Attempts 1
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: name 'ok' is not defined
>>> 
>>> with retry(retries=3):
...     open('/file')
... 
Attempts 1
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
IOError: [Errno 2] No such file or directory: '/file'

これらの例外をインターセプトして、コンテキストマネージャ内で処理する方法はありますか?

18
Mauro Baraldi

引用 __exit__

例外が提供され、メソッドが例外を抑制したい場合(つまり、伝播されないようにする場合)、true値を返します。それ以外の場合、このメソッドの終了時に例外は通常どおり処理されます。

デフォルトでは、関数から明示的に値を返さない場合、Pythonは偽の値であるNoneを返します。あなたの場合、__exit__Noneを返します。そのため、例外は__exit__を通過できます。

だから、このような真実の値を返す

class retry(object):

    def __init__(self, retries=0):
        ...


    def __enter__(self):
        ...

    def __exit__(self, exc_type, exc_val, traceback):
        print 'Attempts', self.attempts
        print exc_type, exc_val
        return True                                   # or any truthy value

with retry(retries=3):
    print ok

出力は

Attempts 1
<type 'exceptions.NameError'> name 'ok' is not defined

再試行機能が必要な場合は、このようにジェネレーターで実装できます。

def retry(retries=3):
    left = {'retries': retries}

    def decorator(f):
        def inner(*args, **kwargs):
            while left['retries']:
                try:
                    return f(*args, **kwargs)
                except NameError as e:
                    print e
                    left['retries'] -= 1
                    print "Retries Left", left['retries']
            raise Exception("Retried {} times".format(retries))
        return inner
    return decorator


@retry(retries=3)
def func():
    print ok

func()
22
thefourtheye

__enter__メソッドの例外を処理するには、withステートメント自体をtry-except句でラップし、単に例外-

しかし、withブロックは、このように機能するように設計されていません。つまり、それ自体が「再帰可能」であるため、ここでは誤解があります。

def __enter__(self):
    for _ in range(self.retries):
        try:
            self.attempts += 1
            return self
        except Exception as e:
            err = e

そこにselfを返すと、コンテキストは__enter__でした実行はもう存在しません-withブロック内でエラーが発生した場合、__exit__に自然に流れます方法。いいえ、とにかく、__exit__メソッドは、実行フローをwithブロックの先頭に戻すことはできません。

あなたはおそらくこのような何かを望んでいるでしょう:

class Retrier(object):

    max_retries = 3

    def __init__(self, ...):
         self.retries = 0
         self.acomplished = False

    def __enter__(self):
         return self

    def __exit__(self, exc, value, traceback):
         if not exc:
             self.acomplished = True
             return True
         self.retries += 1
         if self.retries >= self.max_retries:
             return False
         return True

....

x = Retrier()
while not x.acomplished:
    with x:
        ...
8
jsbueno

これは簡単だと思いますが、他の人々はそれを考えすぎているようです。リソース取得コードを__enter__を返してみてください。selfではなく、フェッチしたリソースです。コードで:

def __init__(self, retries):
    ...
    # for demo, let's add a list to store the exceptions caught as well
    self.errors = []

def __enter__(self):
    for _ in range(self.retries):
        try:
            return resource  # replace this with real code
        except Exception as e:
            self.attempts += 1
            self.errors.append(e)

# this needs to return True to suppress propagation, as others have said
def __exit__(self, exc_type, exc_val, traceback):
    print 'Attempts', self.attempts
    for e in self.errors:
        print e  # as demo, print them out for good measure!
    return True

今それを試してください:

>>> with retry(retries=3) as resource:
...     # if resource is successfully fetched, you can access it as `resource`;
...     # if fetching failed, `resource` will be None
...     print 'I get', resource
I get None
Attempts 3
name 'resource' is not defined
name 'resource' is not defined
name 'resource' is not defined
4
gil