It looks like the Ruby-specific elements here are (1) the 'retry' statement, and (2) the ability to yield a block twice (Python's equivalent `with` syntax explicitly prohibits re-yielding a block).
(1) might just be a matter of taste -- that 'retry', to me, makes me now have to step back and think "oh, I'm retrying something now? what exactly is being retried here?" Having it all wrapped up in a 'while' loop makes the intent explicit right from the get-go.
(2) I agree is nice. It would be great if you could do this:
with backoff():
connect_to_a_thing()
but you're not allowed to 'yield' twice inside a context manager. However you can get damn close (and fully composable) by wrapping the connection logic and the retry logic in functions:
def connect_to_a_thing(url, access_token):
...
def backoff(connector, wait=5, exponent=1.5, *args, **kwargs):
success = False
while not success:
try:
connector(*args, **kwargs)
except BackoffError:
sleep(wait)
wait = wait ** exponent
else:
success = True
backoff(connect_to_a_thing, url, access_token)
Or you could be even more explicit with recursion:
def backoff(connector, wait=5, exponent=1.5, *args, **kwargs):
try:
connector(*args, **kwargs)
except BackoffError:
sleep(wait)
backoff(connector, wait ** exponent, exponent, *args, **kwargs)
As for your point about loop indices, Python generally has wonderful support for
not worrying about loop indices and counters. I'm also not sure how that applies in this case; you still need to increment the value of 'wait'. Then again...
def exponentiate_forever(value, exponent):
while True:
yield value
value = value ** exponent
def backoff(connector, wait=5, exponent=1.5, *args, **kwargs):
for wait in exponentiate_forever(wait, exponent):
try:
connector(*args, **kwargs)
except BackoffError:
sleep(wait)
else:
break
But please don't do that.