Itertools Recipe: Round Robin
The itertools
docs has a ton of slick recipes for using the library to good effect. Some of the code is more useful than illustrative, so I wanted to use these notebooks to break down a few of the functions.
This is
# poor import style, but I want to copy-paste the code
# as-is from the docs
from itertools import *
import itertools
roundrobin()
def roundrobin(*iterables):
"roundrobin('ABC', 'D', 'EF') --> A D E B F C"
# Recipe credited to George Sakkis
num_active = len(iterables)
nexts = cycle(iter(it).__next__ for it in iterables)
while num_active:
try:
for next in nexts:
yield next()
except StopIteration:
# Remove the iterator we just exhausted from the cycle.
num_active -= 1
nexts = cycle(islice(nexts, num_active))
Demo
The second value should be sequential, b
dropping out, and then a
player_a = ['a1', 'a2', 'a3']
player_b = ['b1', 'b2']
player_c = ['c1', 'c2', 'c3', 'c4']
for item in roundrobin(player_a, player_b, player_c):
print(item, end=' ')
a1 b1 c1 a2 b2 c2 a3 c3 c4
Mississippi… sort of
m = 'm'
i = 'iii'
s = 'ssss'
p = 'pp'
for item in roundrobin(m, i, s, p):
print(item, end='')
mispispiss
lol
Why this works
This one relies heavily on some Functional Programming Magicâ„¢, but beyond the main trick, the function is easy enough to understand.
The nexts
iterable
The function starts off by creating a straight-forward num_active
variable that tracks how many iterables we’re juggling.
The nexts
line merits some unpacking:
- At the outer level, it invokes the
itertools.cycle()
method to repeatedly yield the next result of each iterable - We wrap each value
it
in theiter()
, on the off chance one of*iterables
was a standalone string or something - Finally magic part, outlined above is in the fact that we’re cycling through
__next__()
functions, not the values that they yield
From here, we execute the loop while there are still values to yield in any of the iterables
Using and redefining nexts
Because it’s built using itertools.cycle()
, the for next in nexts:
portion will loop indefinitely, yielding the next value of each iterable, until the first time that it exhausts one of them, which is why we’ve got it stuffed inside the try
block
At that point, we catch the StopIteration
exception, decrement our number of active iterables by one, then do some more clever Functional Programming Magicâ„¢.
Revisiting the player_a
, player_b
, player_c
example from above, after looping a couple times, we’ve yielded a1 b1 c1 a2 b2 c2 a3
, pushed player_a
to the back of the cycle, and are getting ready to serve the next value of player_b
B:
C: c3, c4
A:
However, player_b
’s out of values and is about to kick us the StopIteration
exception
C: c3, c4
A:
B: StopIteration
But when it does that, it, critically, gets moved to the back of the cycle
At that point, we build a new cycle
using itertools.islice()
. We still use the nexts
iterable, but because we decremented num_activive
in the previous line, this means that we’re cycling through only the first n-1
iterables in nexts
.
Which, in this case, is player_a
and player_c
– we’ve discarded the empty player_b
from the rotation