6

What is the most elegant way to repeat something after it caused an exception in python?

I have something like this [pseudo code as an example]:

try:
  do_some_database_stuff()
except DatabaseTimeoutException:
  reconnect_to_database()
  do_some_database_stuff() # just do it again

But imagine if I don't have a nice function but a lot of code instead. Duplicate code is not very nice.

So I think this is slightly better:

while True:
  try:
    do_some_database_stuff()
    break
  except DatabaseTimeoutException:
    reconnect_to_database()

That's good enough if the exception really fixes the problem. If not I need a counter to prevent an indefinite loop:

i = 0
while i < 5:
  try:
    do_some_database_stuff()
    break
  except DatabaseTimeoutException:
    reconnect_to_database()
    i += 1

But then I don't really know if it worked so it's also:

while i <= 5:
  try:
    do_some_database_stuff()
    break
  except DatabaseTimeoutException:
    if i != 5:
     reconnect_to_database()
    else:
      raise DatabaseTimeoutException
    i += 1

As you can see it starts to get very messy.

What is the most elegant way of expressing this logic?

  • try something
  • if it fails apply fix
  • try n more times including the fix
  • if it continues to fail give me an error to prevent a indefinite loop
b.b3rn4rd
  • 8,494
  • 2
  • 45
  • 57
JasonTS
  • 2,479
  • 4
  • 32
  • 48

2 Answers2

6

You can use a "for-else" loop:

for ii in range(5):
    try:
        do_some_database_stuff()
        break
    except DatabaseTimeoutException:
        reconnect_to_database()
else:
    raise DatabaseTimeoutException

Or, without:

for ii in range(5):
    try:
        do_some_database_stuff()
        break
    except DatabaseTimeoutException:
        if ii == 4:
            raise
        reconnect_to_database()
Community
  • 1
  • 1
John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • The top answer to that questions states "But anytime you see [a for-else] construct, a better alternative is...". I agree. Better to wrap the code block in a function and use return rather than break. – Dunes Nov 07 '14 at 09:05
  • 1
    @Dunes: that's why I provided an alternative, for those who find the for-else a bit too much. I'm on the fence, personally, it's certainly a matter of taste. – John Zwinck Nov 07 '14 at 12:04
  • 1
    The `for-else` version isn't equivalent as it'll run `reconnect_to_database` 5 times on 5 failures instead of 4. – Veedrac Nov 07 '14 at 21:34
1

I'm personally not a fan of the for-else construct. I don't think it's intutitive. First time I read it I thought it meant "do for loop (...), if iterable was empty then ...".

You should place your code inside a function. If do_some_database_stuff() completes successfully then you can use the return statement to return early from the function.

eg.

def try_to_do_some_database_stuff():
    for i in range(num_times):
        try:
            return do_some_database_stuff()
        except DatabaseTimeoutException:
            reconnect_to_database()
    raise DatabaseTimeoutException

If you find yourself using this construct quite a lot then you can make it more generic.

def try_to_do(func, catch, times=2, on_exception=None):
    for i in range(times):
        try:
            return func()
        except catch:
            if on_exception:
                on_exception()
    raise catch

try_to_do(do_some_database_stuff, catch=DatabaseTimeoutException, times=5, 
    on_exception=reconnect_to_database)

You can use functools.partial if you need to pass arguments to your functions.

Dunes
  • 37,291
  • 7
  • 81
  • 97
  • This will run `reconnect_to_database` 5 times for 5 failures which seems a bit pointless. – Veedrac Nov 07 '14 at 21:45
  • It's copying the behaviour of the loops in the question. An additional `i+1 < times` in the if statement would suffice to change the behaviour to not respond to the last exception. – Dunes Nov 09 '14 at 13:13