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 itertoolsroundrobin()
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
itin theiter(), on the off chance one of*iterableswas 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