Python iterators that timeout
Images can’t replace code
Iterators is the one thing that makes looping or iteration so easy and beautiful in python. Many of us use it all the time without noticing where and how it’s working.
Simple usage of iterators involves using a for loop to run through the elements of an iterable like a list, dictionary or any kind of type that supports iteration. Mostly you write something like this.
a = [1,2,3]
for v in a:
print(v)
Below one shows explicit use of iterator.
a = [1,2,3]
for v in iter(a):
print(v)
And without a for
loop, things look something like this:
a = [1,2,3]
it = iter(a)try:
while True:
v = next(it)
print(v)
except StopIteration:
pass
Examples like above are simple cases where we loop over something that always yields or gives you an element as you request it. There is very minimal time delay in fetching results. Consider a case, where your results are being generated by some logic which takes time to process and yield results, or fetches it from another server and then yields it, anything which takes some perceivable amount of time.
Lets do a small simulation of something like that.
import timedef network_process():
yield 1
time.sleep(1)
yield 2
time.sleep(1)
yield 3
for i in network_process():
print(i)
If you ran this piece of code yourself, you would clearly notice that it takes more time since the iterator yielding the values sleeps for some time. In this case, the sleep time if fixed. In actual systems, this time can vary.
In some very special cases, you may want that the iterator yields the next element within a timebound. And if it fails to do so, it should return a None or something that can be distinguish a timeout from a normal iteration result. And after timeout, you can continue normal iteration with the same iterator till the iterator gets exhausted.
Timeout Iterator
For such cases I came up with a solution of a TimeoutIterator class. This class wraps an iterator or anything behaving like an iterator (generators etc.) with and added feature of timeout. This still gives you the python’s iterator interface, with additional methods to adjust timeout.
To use it, lets first get the thing from pypi.
pip install iterators
When I say it behaves like an Iterator, I mean that it will raise a StopIteration exception. If the underlying iterator itself raises an exception, it will be propagated etc.
Simple use looks something like this:
import timefrom iterators import TimeoutIteratordef network_process():
yield 1
time.sleep(1)
yield 2
time.sleep(1)
yield 3for i in TimeoutIterator(network_process()):
print(i)
This new code runs exactly like our previous code and produces same output. Now lets add a timeout of 0.9 seconds.
import timefrom iterators import TimeoutIteratordef network_process():
yield 1
time.sleep(1)
yield 2
time.sleep(1)
yield 3for i in TimeoutIterator(network_process(), timeout=0.9):
print(i)
The output generate looks something like this:
1
<object object at 0x7fce6d761c00>
2
<object object at 0x7fce6d761c00>
3
Basically we got a timeout two times, and each time the result given was some object. Lets see what this object is.
import timefrom iterators import TimeoutIteratordef network_process():
yield 1
time.sleep(1)
yield 2
time.sleep(1)
yield 3it = TimeoutIterator(network_process(), timeout=0.9)
for i in it:
if i == it.get_sentinel():
print("Timeout")
else:
print(i)
Run this code yourself, and you will find that each time a timeout happens, we get a sentinel object. The same object is returned each time. get_seninal()
returns the sentinal object for this instance of TimeoutIterator. This helps to identify when a timeout happens vs normal iteration.
Customizing Sentinel behavior
import timefrom iterators import TimeoutIteratordef network_process():
yield 1
time.sleep(1)
yield 2
time.sleep(1)
yield 3for i in TimeoutIterator(network_process(), timeout=0.9, sentinel=None):
print(i)
Here we are telling that we want a None to be returned whenever a timeout happens. This can be customized to any kind of object except for any kind of Exception
classes. So the output of above looks like this:
1
None
2
None
3
Customizing Timeouts
import timefrom iterators import TimeoutIteratordef network_process():
yield 1
time.sleep(1)
yield 2
time.sleep(1)
yield 3it = TimeoutIterator(network_process(), timeout=0.9, sentinel=None)
for i in it:
print(i)
if i == 2:
it.set_timeout(0.0)
For this code, we set the value of timeout to 0.0
i.e. no timeout. So after first timeout, the iterator will block and wait for an actual timeout. Look at the output below.
1
None
2
3
And that’s it. If you find this useful anywhere I’ll be really glad to know about it, please do remember to drop a comment below.
Also please reach out for any help. Cheers 🍻