pythonic way to try (repeatedly) to catch exceptions
Question: I have a script that runs on an unattended machine. If the script throws an
error, I want to log the error and keep running as long as it's practical.
Here's my first draft of the code, but it doesn't work for reasons explained
below. (As a note: a more realistic `concede` function will return true if
there are N errors captured within T seconds. Or something.)

    
    
    from contextlib import contextmanager
    
    @contextmanager
    def perseverance(concede = lambda: False):
        while (True):
            try:
                yield
            except Exception, e:
                if (concede()):
                    log_exception(e, "conceding")
                    raise
                else:
                    log_exception(e, "retrying")
    

This would allow me to do something like:

    
    
    def quit_after(n):
        n = [n]         # make n mutable for the closure :P
        def quitter():
            if (n[0] <= 0): return True
            n[0] -= 1
            return False
        return quitter
    
    with perseverance(quit_after(3)):
        do_complex_script()
    

This doesn't work because the @contextmanager is fussy about exception
handling. This code will fail with:

    
    
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
      File ".../python2.7/contextlib.py", line 36, in __exit__
        raise RuntimeError("generator didn't stop after throw()")
    RuntimeError: generator didn't stop after throw()
    

I could trivially rewrite perseverance() to not use context manager but
without its elegant syntax, like this:

    
    
    def with_perseverance(fn, concede = lambda: False):
        while (True):
            try:
                fn()
            except Exception, e:
                if (concede()):
                    log_exception(e, "conceding")
                    raise
                else:
                    log_exception(e, "retrying")
    

and call it like this:

    
    
    with_perseverance(do_complex_script, quit_after(3))
    

## the question

At the risk of getting down-voted for being too subjective: Is there a better
/ more pythonic way to write this?


Answer: The `contextmanager` isn't fussy about exception handling; it just wants you
to only `yield` once. Context managers don't support reentry.

If you want that, you have several options. One is to use a `with`-`for`
combo:

    
    
    from contextlib import contextmanager
    
    class MutableValue:
        def __init__(self, value):
            self.value = value
    
    @contextmanager
    def null_context():
        yield
    
    @contextmanager
    def catch_and_log(mutable_return_successful):
        try:
            yield
        except Exception as e:
            print("ERROR:", e)
        else:
            mutable_return_successful.value = True
    
    def quit_after(n):
        for _ in range(n-1):
            successful = MutableValue(False)
            yield catch_and_log(successful)
            if successful.value:
                return
    
        yield null_context()
    
    for ctx in quit_after(5):
        with ctx:
            1/0
    
    #>>> ERROR: division by zero
    #>>> ERROR: division by zero
    #>>> ERROR: division by zero
    #>>> ERROR: division by zero
    #>>> Traceback (most recent call last):
    #>>>   File "", line 31, in <module>
    #>>> ZeroDivisionError: division by zero
    

Because the `with` needs to communicate with the `for`, I had to do a bit of
hackery to get the returns right. It's still more readable than a class-based
solution.

Another much easier option is to abuse decorators:

    
    
    def quit_after(n):
        def inner(f):
            for _ in range(n-1):
                try:
                    f()
                except Exception as e:
                    print("ERROR:", e)
                else:
                    return
            f()
    
        return inner
    
    @quit_after(5)
    def _():
        1/0
    #>>> ERROR: division by zero
    #>>> ERROR: division by zero
    #>>> ERROR: division by zero
    #>>> ERROR: division by zero
    #>>> Traceback (most recent call last):
    #>>>   File "", line 44, in <module>
    #>>>   File "", line 40, in inner
    #>>>   File "", line 46, in _
    #>>> ZeroDivisionError: division by zero
    

