My last post was educational for me, but it turned out to be not quite correct.
There was a subtle issue that caused some confusion and an out-and-out bug. We’ll start with the bug first
return value of next
A friend pointed out to me that the usage of next was incorrect. Let’s look at the function we dissected last week:
def merge_lists_refactor(lists, labels):
objects = list(map(next, lists))
while any(x is not None for x in objects):
i, obj = min(enumerate(objects), key=lambda x: (x[1]['time'], x[0]))
yield labels[i], obj
objects[i] = next(lists[i])
The behavior of the final line is where the bug crept in. The call to next is written assuming that next returns None when it’s finished generating values. This is not the case, of course it throws a StopIteration exception. At first I was a little confused at how I missed this. I had run the program over the dataset I was concerned with and it produced output.
That was the first mistake. Instead of generating a small, focused, meaningful test, I simply ran the code against two large log files. It generated output (lots of it – the log files were each about 20,000 lines long), which I deemed “correct”. The friend that pointed this out had tested with a clear data set which a small number of output lines which made it obvious that the program wasn’t running.
I was also confused as to how I didn’t see the exception bubbling up to the top and crashing my program. It turns out my test program was calling the generator as part of a for loop and the StopIteration exception was caught there.
for label, line in merge_lists_refactor([lines_one, lines_two],\
[file_one, file_two]):
The communication issue
The other issue my reviewer had was one of poor communication on my part.
I not only didn’t provide a docstring for the function (mainly for brevity) I also did not show an example of calling it. He attempted to call the function with lists (which seems like a reasonable thing to do), but the function itself was designed to work with two generators, not two lists. The communication problem will be addressed below. Fixing the code to run on both lists and generators is left as an exercise for another day.
A generator to merge generators
Here’s the final version of the code, complete with example usage.
def merge_lists_refactor(lists, labels):
objects = list(map(next, lists))
while objects:
i, obj = min(enumerate(objects), key=lambda x: (x[1]['time'], x[0]))
yield labels[i], obj
try:
objects[i] = next(lists[i])
except StopIteration:
del objects[i]
del labels[i]
del lists[i]
list(merge_lists_refactor([iter(testa),iter(testb)], ['foo', 'bar']))
Note: there are many built-in solutions for this problem that are better for solving this. This is not intended as a “here’s how to do this” answer, but an educational journey.

