Sometimes I need to be able to split data into chunks, and so something like str.split
would be helpful. This comes with two downsides:
- Input has to be strings
- You consume all input when generating the output.
I have a couple of requirements:
- It needs to work with any iterable / iterator. Where the items have the
!=
comparator. - I don't want to consume the chunk of data when returning it.
Rather than returning a tuple, I need to return a generator.
And so this left me with two ways to implement the code. A fully lazy version isplit
. And one that is semi-lazy where it consumes some of the generator, when moving to the next chunk, without fully consuming it.
And so I created:
from __future__ import generator_stop
import itertools
def _takewhile(predicate, iterator, has_data):
"""
Return successive entries from an iterable as long as the
predicate evaluates to true for each entry.
has_data outputs if the iterator has been consumed in the process.
"""
for item in iterator:
if predicate(item):
yield item
else:
break
else:
has_data[0] = False
def isplit(iterator, value):
"""Return a lazy generator of items in an iterator, seperating by value."""
iterator = iter(iterator)
has_data = [True]
while has_data[0]:
yield _takewhile(value.__ne__, iterator, has_data)
def split(iterator, value):
"""Return a semi-lazy generator of items in an iterator, seperating by value."""
iterator = iter(iterator)
has_data = [True]
while True:
carry = []
d = _takewhile(value.__ne__, iterator, has_data)
try:
first = next(d)
except StopIteration:
if not has_data[0]:
break
yield iter([])
else:
yield itertools.chain([first], d, carry)
carry.extend(d)
An example of these working are below. There is an edge case with isplit
, which is as far as I know inherent from the code being fully lazy. This is shown below too.
print('isplit')
print([list(i) for i in isplit('abc def ghi', ' ')])
print([list(i) for i in isplit(' abc def ghi', ' ')])
s = isplit('abc def ghi', ' ')
print(list(itertools.zip_longest(*itertools.islice(s, 4))))
print('\nsplit')
print([list(i) for i in split('abc def ghi', ' ')])
print([list(i) for i in split(' abc def ghi', ' ')])
s = split('abc def ghi', ' ')
print(list(itertools.zip_longest(*itertools.islice(s, 4))))
Which outputs:
isplit
[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]
[[], ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]
[('a', 'b', 'c', None), ('d', 'e', 'f', None), (None, 'g', 'h', None), (None, 'i', None, None)]
split
[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]
[[], ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]
[('a', 'd', 'g'), ('b', 'e', 'h'), ('c', 'f', 'i')]
has_data
necessary? Furthermore, why is it a list of booleans instead of just a boolean? If you get rid ofhas_data
you can useitertools.takewhile
also... \$\endgroup\$has_data
, or change it to just a boolean, then the code will return an infinite amount of empty generators... \$\endgroup\$