2

I need to check for a certain condition before a timeout. If the condition is met before the limit then I return True, otherwise I return False.

I'm doing this in the following way:

def foobar():
    counter = 1
    condition_met = False
    while counter < max_limit:
        if <conditions are met>:
            condition_met = True
            break
        time.sleep(10)
        counter += 1
    return condition_met

I'm wondering if there's a more pythonic way to do the same thing.

martineau
  • 119,623
  • 25
  • 170
  • 301
Simon
  • 323
  • 5
  • 15

4 Answers4

3

A really good way to time things is by using—shock—the time module:

import time

def foo():
    max_limit = 25  # Seconds.

    start = time.time()
    condition_met = False
    while time.time() - start < max_limit:
        if <conditions are met>:
            condition_met = True
            break
        time.sleep(10)

    return condition_met

See? The module is good for more than just sleeping. ;¬)

martineau
  • 119,623
  • 25
  • 170
  • 301
2

I am assuming you have a code in a function. The below code get rid of a variable condition_met and break statement.

counter = 1
# condition_met = False
while counter < max_limit:
    if <conditions are met>:
        # condition_met = True
        return True   # get rid of break statement
    time.sleep(10)
    counter += 1
return False
Sociopath
  • 13,068
  • 19
  • 47
  • 75
  • 1
    You could save two more lines and a variable by just looping `for _ in range(max_limix):` (assuming `counter` is not used in the condition) – tobias_k Dec 07 '18 at 09:39
  • Thanks, indeed you're right but my bad i forgot to add that after the `while` I print a message whether conditions are met or we reach a timeout but checking condition_met value. Using your solution I'd need to do the check outside my function and at the end it'd almost be the same thing but I prefer yours because it removes a variable – Simon Dec 07 '18 at 09:42
  • 1
    @Simon Instead of `True` and `False` you can return the message that you want to print. – Sociopath Dec 07 '18 at 09:45
  • True again but in my case I need to quit my program and print a Warning if False. So I still need to check the value returned. – Simon Dec 07 '18 at 09:58
  • Incrementing a counter isn't a very accurate way of timing something, it's just a count. In this case there is a relationship between elapsed time and loop iterations because of the `time.sleep()` call in the loop, but in general it will depend on how fast the computer can execute the other statements within the loop. To actually _time_ how long something is or has taken requires determining the amount of time that has elapsed since some starting point. – martineau Dec 12 '18 at 09:02
2

If it were not for the time.sleep, your loop would be equivalent to

for _ in range(max_limit):
    if <condition>:
        return True
    # time.sleep(10)
return False

Which is equivalent to return any(<condition> for _ in range(max_limit).

Thus, you could (ab)use any and or to check whether the condition is met up to a certai number of times while waiting a bit before each check:

any(time.sleep(10) or <condition> for _ in range(max_limit))

This will first evaluate time.sleep, which returns None, and then evaluate the condition, until the condition is met or the range is exhausted.

The only caveat is that this will call time.sleep even before the first check of the condition. To fix this, you can first check the counter variable and only if that is > 0 call time.sleep:

any(i and time.sleep(10) or <condition> for i in range(10))

Whether that's clearer than the long loop is for you to decide.


As suggested in comments, you can in fact just invert the above any clause to

any(<condition> or time.sleep(10) for _ in range(max_limit))

This will first check the condition and only if the condition is false will sleep. It also ready much more naturally than any of the above two appraoches.

tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • Thanks Tobias, I'm voiceless – Simon Dec 07 '18 at 10:01
  • 1
    @Simon I am very sorry to hear that. Or did you mean speechless? ;-) – tobias_k Dec 07 '18 at 10:09
  • 1
    Wouldn't the first solution already work if rewritten as `any( or time.sleep(10) for _ in range(max_limit))`? – lekv Feb 20 '19 at 01:31
  • @lekv Absolutely, and it also ready much more naturally. Makes me wonder why I missed that. Thanks! (I _think_ I thought "hm, but we don't know if will be true or false, so we can't put the `or` after or it may not be evaluated", but of course that's just how we want it.) – tobias_k Feb 20 '19 at 09:12
0

Here's another way to do it that uses a threading.Timer in conjunction with a function decorator to know when a given time limit has been exceeded.

I adapted the approach in @Aaron Hall's answer to a very similar Timeout on a function call question so it can be used without killing the main thread (i.e. the caller) in the process.

In this scheme, a Timer object is created to call a quit function after a time_limit number of seconds has elapsed. If that happens the quit function prints out a notification message and then causes a KeyboardInterrupt exception to be raised by calling _thread.interrupt_main(). The decorator() function wrapped around the original function catches this exception and makes it appear that the function returned False — conversely, if that doesn't happen, it cancels the Timer thread and returns True.

from random import randint
import sys
import time
import threading
import _thread as thread  # Low-level threading API


def interupt_after(time_limit):
    ''' Decorator to raise KeyboardInterrupt if function takes more than
        `time_limit` seconds to execute.
    '''
    def quit_func(func_name):
        print('{0}() took too long'.format(func_name), file=sys.stderr)
        sys.stderr.flush()
        thread.interrupt_main()  # Raises KeyboardInterrupt.

    def decorator(func):
        def decorated(*args, **kwargs):
            timer = threading.Timer(time_limit, quit_func, args=[func.__name__])
            timer.start()
            try:
                func(*args, **kwargs)  # Note return value ignored.
            except KeyboardInterrupt:
                return False
            finally:
                timer.cancel()
            return True
        return decorated

    return decorator


if __name__ == '__main__':

    @interupt_after(5)
    def foo(lo, hi):
        mid = (lo+hi) // 2
        condition_met = False
        while not condition_met:
            if randint(lo, hi) == mid:  # Condition to meet.
                condition_met = True
                break
            time.sleep(1)

        return condition_met

    print(f'foo(40, 44):', foo(40, 44))
    print('-fini-')

It prints this when the condition is met in the allotted time:

foo(40, 44): True
-fini-

and this when it's not:

foo() took too long
foo(40, 44): False
-fini-
martineau
  • 119,623
  • 25
  • 170
  • 301