Ленивый Сплит и полу-ленивый сплит


Иногда мне нужно быть в состоянии разделить данные на блоки, и так что-то вроде str.split было бы очень полезно. Это происходит с двух недостатков:

  1. Входной сигнал должен быть строк
  2. Вы потребляете все входные при формировании выходных данных.

У меня есть пару требований:

  • Он должен работать с любым итерируемым / итератор. Где элементы имеют != компаратор.
  • Я не хочу потреблять блока данных при возвращении его.
    А не возвращает кортеж, мне нужно вернуть генератор.

И так это у меня осталось два пути реализации код. Полностью ленивый вариант isplit. И тот, который полу-лазы, где он потребляет некоторые из генератора, при переходе на следующий фрагмент, не полностью поглотив ее.

И так я создал:

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)

Пример работы ниже. Есть пограничный случай с isplit, который насколько я знаю, неотъемлемое от код полностью ленивый. Это тоже показано ниже.

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))))

Выходы:

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')]


691
8
задан 21 февраля 2018 в 05:02 Источник Поделиться
Комментарии
2 ответа


  1. Я бы предпочел название iterable для итерируемый аргумент (сравните документация itertools модуля), и sep для разделитель аргументов (сравните документация str.split).

  2. isplit имеет неудовлетворительную характеристику, что вы не можете игнорировать любого из возвращенных итераторы — вы должны потреблять каждый полностью, прежде чем перейти к следующему, в противном случае итерация пойдет не так.

    Например, предположим, что мы хотим выбрать все слова, начинающиеся с заглавной буквы. Мы могли бы попробовать:

    for word in isplit('Abc def Ghi', ' '):
    first = next(word)
    if first == first.upper():
    print(first + ''.join(word))

    Но это производит вывод:

    Abc
    Traceback (most recent call last):
    File "<stdin>", line 2, in <module>
    StopIteration

    Вместо этого, мы должны убедиться, что мы потребляем каждый word итератор полностью, даже если мы не заботимся об этом:

    for word in isplit('Abc def Ghi', ' '):
    first = next(word)
    if first == first.upper():
    print(first + ''.join(word))
    else:
    for _ in word:
    pass

    Та же проблема возникает со стандартной библиотекой функций itertools.groupby, где вызывающий код может перейти к следующей группе до его завершения итерации по предыдущей группе. groupby решает эту проблему для нас полностью потреблял предыдущей группе, как только абонент переходит к следующей группе. Это будет полезно для isplit сделать то же самое.


  3. Сходство с itertools.groupby предполагает, что мы могли бы реализовать isplit очень просто с точки зрения groupbyвот так:

    from itertools import groupby

    def isplit(iterable, sep):
    """Generate the contiguous groups of items from the iterable that are
    not equal to sep.

    The returned groups are themselves iterators that share the
    underlying iterable with isplit(). Because the source is shared,
    when the isplit() object is advanced, the previous group is no
    longer visible. So, if that data is needed later, it should be
    stored as a list.

    """
    for key, group in groupby(iterable, sep.__ne__):
    if key:
    yield group

    Обратите внимание, что этот код ведет себя как обычный str.split() в том, что она сращивается соседних сепараторов. Если вам нужно такое поведение больше нравится str.split(' ')С пустыми группами, когда есть рядом сепараторов, то он должен быть простым, чтобы добавить else: предложение создать необходимые пустой итераторы, как это:

    for key, group in groupby(chain((sep,), iterable, (sep,)), sep.__ne__):
    if key:
    yield group
    else:
    for _ in islice(group, 1, None):
    yield iter(())

    Это использует itertools.chain и itertools.islice.

    (Есть пару незначительных оптимизаций можно сделать здесь: 1-элемент кортежа (sep,) может храниться в переменной и использовать дважды, и iter(()) может быть глобальной константы, поскольку вам не нужен новый пустой итератор каждый раз.)


6
ответ дан 22 февраля 2018 в 10:02 Источник Поделиться

Есть ошибка в коде.

>>> print([list(i) for i in split(' abc def ghi ', ' ')])
[[], ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']]

Однако это должно закончиться в пустой список.

Чтобы исправить это, вы только должны изменить while True цикл while has_data[0]. После этого вы можете объединить except и else вместе, это означает, что вам не нужно try на всех. И поэтому вы можете использовать:

def split(iterator, value):
iterator = iter(iterator)
has_data = [True]
while has_data[0]:
carry = []
d = _takewhile(value.__ne__, iterator, has_data)
yield itertools.chain(d, carry)
carry.extend(d)

0
ответ дан 22 февраля 2018 в 11:02 Источник Поделиться