2

I am writing in Python 2.7 and encounter the following situation. I would like to try calling a function three times. If all three times raise errors, I will raise the last error I get. If any one of the calls succeed, I will quit trying and continue immediately.

Here is what I have right now:

output = None
error = None
for _e in range(3):
    error = None
    try:
        print 'trial %d!' % (_e + 1)
        output = trial_function()
    except Exception as e:
        error = e
    if error is None:
        break
if error is not None:
    raise error

Is there a better snippet that achieve the same use case?

Patrick the Cat
  • 2,138
  • 1
  • 16
  • 33

4 Answers4

7

use decorator

from functools import wraps

def retry(times):

    def wrapper_fn(f):

        @wraps(f)
        def new_wrapper(*args,**kwargs):
            for i in range(times):
                try:
                    print 'try %s' % (i + 1)
                    return f(*args,**kwargs)
                except Exception as e:
                    error = e
            raise error

        return new_wrapper

    return wrapper_fn

@retry(3)
def foo():
    return 1/0;

print foo()
powerfj
  • 156
  • 6
  • A decorator is a good idea where *all* calls to a function should be retried - consider including [`functools.wraps`](https://docs.python.org/2/library/functools.html#functools.wraps) to retain docstrings. – jonrsharpe Aug 03 '14 at 16:30
6

Here is one possible approach:

def attempt(func, times=3):
    for _ in range(times):
        try:
            return func()
        except Exception as err:
            pass
    raise err

A demo with a print statement in:

>>> attempt(lambda: 1/0)
Attempt 1
Attempt 2
Attempt 3

Traceback (most recent call last):
  File "<pyshell#18>", line 1, in <module>
    attempt(lambda: 1/0)
  File "<pyshell#17>", line 8, in attempt
    raise err
ZeroDivisionError: integer division or modulo by zero

If you're using Python 3.x and get an UnboundLocalError, you can adapt as follows:

def attempt(func, times=3):
    to_raise = None
    for _ in range(times):
        try:
            return func()
        except Exception as err:
            to_raise = err
    raise to_raise

This is because the err is cleared at the end of the try statement; per the docs:

When an exception has been assigned using as target, it is cleared at the end of the except clause.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • i know its an old answer. As cool as it looks, it seems to no longer work on python 3, and yields `UnboundLocalError: local variable 'err' referenced before assignment` – murison Mar 25 '20 at 15:23
  • 1
    @murison thanks for pointing it out! Interestingly I see that behaviour even if I add `err = None` to the start of the function, something must be happening with the `as` context binding. – jonrsharpe Mar 25 '20 at 15:29
  • yup, i ended up with exacly the same . i dont like that extra overhead... – murison Mar 25 '20 at 15:46
  • Let's say I want to pass a function with an argument to this attempt() function. How can I do this without that function executing before it is passed? To clarify: I do not want to execute the function and pass it's outcome to the attempt() function. – Axel Köhler Dec 15 '20 at 14:30
  • 1
    @AxelKöhler pass a [`partial`](https://docs.python.org/3/library/functools.html#functools.partial). – jonrsharpe Dec 15 '20 at 14:31
3

Ignoring the debug output and the ancient Python dialect, this looks good. The only thing I would change is to put it into a function, you could then simply return the result of trial_function(). Also, the error = None then becomes unnecessary, including the associated checks. If the loop terminates, error must have been set, so you can just throw it. If you don't want a function, consider using else in combination with the for loop and breaking after the first result.

for i in range(3):
    try:
        result = foo()
        break
    except Exception as error:
        pass
else:
    raise error
use_somehow(result)

Of course, the suggestion to use a decorator for the function still holds. You can also apply this locally, the decorator syntax is only syntactic sugar after all:

# retry from powerfj's answer below
rfoo = retry(3)(foo)
result = rfoo()
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
1

Came across a clean way of doing the retries. There is a module called retry.

  • First install the module using

    pip install retry
    
  • Then import the module in the code.

    from retry import retry
    
  • Use @retry decorator above the method, We can pass the parameters to the decorator. Some of the parameters are tries , delay , Exception.

Example

    from retry import retry

    @retry(AssertionError, tries=3, delay=2)
    def retryfunc():
        try:
            ret = False
            assert ret, "Failed"
        except Exception as ex:
            print(ex)
            raise ex
  • The above code asserts and fails everytime, but the retry decorator retries for 3 times with a delay of 2 seconds between retries. Also this only retries on Assertion failures since we have specified the error type as AssertionError on any other error the function wont retry.
Prabhudsp
  • 31
  • 1