разделить генератор/итерацию каждые n элементов в python (splitEvery)

Я пытаюсь написать функцию «splitEvery» Haskell в Python. Вот его определение:

splitEvery :: Int -> [e] -> [[e]]
    @'splitEvery' n@ splits a list into length-n pieces.  The last
    piece will be shorter if @n@ does not evenly divide the length of
    the list.

Базовая версия работает нормально, но мне нужна версия, которая работает с генераторными выражениями, списками и итераторами. И, если на вход подается генератор, он должен возвращать генератор на выходе!

Тесты

# should not enter infinite loop with generators or lists
splitEvery(itertools.count(), 10)
splitEvery(range(1000), 10)

# last piece must be shorter if n does not evenly divide
assert splitEvery(5, range(9)) == [[0, 1, 2, 3, 4], [5, 6, 7, 8]]

# should give same correct results with generators
tmp = itertools.islice(itertools.count(), 10)
assert list(splitEvery(5, tmp)) == [[0, 1, 2, 3, 4], [5, 6, 7, 8]]

Текущая реализация

Вот код, который у меня сейчас есть, но он не работает с простым списком.

def splitEvery_1(n, iterable):
    res = list(itertools.islice(iterable, n))
    while len(res) != 0:
        yield res
        res = list(itertools.islice(iterable, n))

Это не работает с выражением генератора (спасибо jellybean за исправление):

def splitEvery_2(n, iterable): 
    return [iterable[i:i+n] for i in range(0, len(iterable), n)]

Должен быть простой фрагмент кода, который выполняет разбиение. Я знаю, что у меня могут быть просто разные функции, но кажется, что это должно быть легко и просто. Я, вероятно, застрял на неважной проблеме, но это действительно беспокоит меня.


Он похож на группировщик из http://docs.python.org/library/itertools.html#itertools.groupby, но я не хочу, чтобы он заполнял лишние значения.

def grouper(n, iterable, fillvalue=None):
    "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return izip_longest(fillvalue=fillvalue, *args)

В нем упоминается метод, который усекает последнее значение. Я тоже не этого хочу.

Порядок вычисления итерируемых объектов слева направо гарантирован. Это делает возможной идиому для кластеризации ряда данных в группы n-длины с использованием izip(*[iter(s)]*n).

list(izip(*[iter(range(9))]*5)) == [[0, 1, 2, 3, 4]]
# should be [[0, 1, 2, 3, 4], [5, 6, 7, 8]]

person James Brooks    schedule 16.12.2009    source источник
comment
связанный Каков самый «питоновский» способ перебора списка по частям? stackoverflow.com/questions/434287/   -  person jfs    schedule 21.05.2010


Ответы (12)


from itertools import islice

def split_every(n, iterable):
    i = iter(iterable)
    piece = list(islice(i, n))
    while piece:
        yield piece
        piece = list(islice(i, n))

Некоторые тесты:

>>> list(split_every(5, range(9)))
[[0, 1, 2, 3, 4], [5, 6, 7, 8]]

>>> list(split_every(3, (x**2 for x in range(20))))
[[0, 1, 4], [9, 16, 25], [36, 49, 64], [81, 100, 121], [144, 169, 196], [225, 256, 289], [324, 361]]

>>> [''.join(s) for s in split_every(6, 'Hello world')]
['Hello ', 'world']

>>> list(split_every(100, []))
[]
person Roberto Bonvallet    schedule 16.12.2009
comment
См. мой ответ для однострочной версии без гражданства, основанной на этой. - person Elliot Cameron; 07.04.2014
comment
проверьте мой ответ для еще более простого однострочного (python 3) и двухстрочного в python 2 - person acushner; 17.12.2016
comment
Посмотрите мое однострочное решение, которое также можно встроить. - person Andrey Cizov; 06.07.2017
comment
+1 Все однострочники в комментариях выше создают бесконечный цикл, если передается последовательность, например. range(), или они не являются более однострочными, если проблема была исправлена. Это кажется по-прежнему лучшим ответом. - person hynekcer; 22.10.2017
comment
Заметным решением является Эшли Уэйт, важная для огромных n. Она единственная приняла требование: если на вход подается генератор, он должен возвращать генератор на выходе! - person hynekcer; 22.10.2017
comment
@ElliotCameron ваше исходное решение не совсем не имеет состояния, потому что оно принимает и использует итератор, объект с состоянием. - person Eli Korvigo; 02.01.2018

Вот краткая однострочная версия. Как и Haskell, он ленив.

from itertools import islice, takewhile, repeat
split_every = (lambda n, it:
    takewhile(bool, (list(islice(it, n)) for _ in repeat(None))))

Для этого необходимо использовать iter перед вызовом split_every.

Пример:

list(split_every(5, iter(xrange(9))))
[[0, 1, 2, 3, 4], [5, 6, 7, 8]]

Хотя приведенная ниже версия не является однострочной, она не требует, чтобы вы вызывали iter, что может быть распространенной ошибкой.

from itertools import islice, takewhile, repeat

def split_every(n, iterable):
    """
    Slice an iterable into chunks of n elements
    :type n: int
    :type iterable: Iterable
    :rtype: Iterator
    """
    iterator = iter(iterable)
    return takewhile(bool, (list(islice(iterator, n)) for _ in repeat(None)))

(Спасибо @eli-korvigo за улучшения.)

person Elliot Cameron    schedule 07.04.2014
comment
Настоящим я даю вам значок [Возрождение]! =р - person justhalf; 02.05.2014
comment
Почему вместо def split_every(n, it): используется лямбда? - person cfh; 07.04.2017
comment
Цель состояла в том, чтобы быть однострочным, но в итоге я поставил его на два на SO, чтобы предотвратить прокрутку. - person Elliot Cameron; 09.04.2017
comment
Это создает бесконечный цикл с последовательностью, например. с диапазоном()/xrange(). - person hynekcer; 22.10.2017
comment
@hynekcer Я загрузил обновленное решение, которое не - person Eli Korvigo; 02.01.2018
comment
@hynekcer Сначала вам нужно позвонить iter в диапазоне. Однако в обновленной версии @EliKorvigo нет этой ловушки, которая мне нравится. - person Elliot Cameron; 05.01.2018
comment
@EliKorvigo Мне больше нравится твоя версия, поэтому я обновил свою. - person Elliot Cameron; 05.01.2018

more_itertools имеет chunked:

import more_itertools as mit


list(mit.chunked(range(9), 5))
# [[0, 1, 2, 3, 4], [5, 6, 7, 8]]
person pylang    schedule 13.05.2017

построение принятого ответа и использование менее известного использования iter (что при передаче второго аргумента он вызывает первый, пока не получит второй), вы можете сделать это очень легко:

питон3:

from itertools import islice

def split_every(n, iterable):
    iterable = iter(iterable)
    yield from iter(lambda: list(islice(iterable, n)), [])

питон2:

def split_every(n, iterable):
    iterable = iter(iterable)
    for chunk in iter(lambda: list(islice(iterable, n)), []):
        yield chunk
person acushner    schedule 17.12.2016
comment
Это не для общего итерируемого. Он работает на генераторе, но создает бесконечный цикл в последовательности. - person hynekcer; 22.10.2017
comment
@hynekcer хороший звонок. отредактировал его, чтобы он не зацикливался бесконечно на не-генераторах. - person acushner; 22.10.2017
comment
В ПОРЯДКЕ. Нелегко написать читаемый однострочный текст, но интересно использовать iter. - person hynekcer; 22.10.2017

Я столкнулся с этим, когда тоже пытаюсь нарезать партии, но делаю это на генераторе из потока, поэтому большинство решений здесь неприменимы или не работают в python 3.

Для людей, которые все еще натыкаются на это, вот общее решение с использованием itertools:

from itertools import islice, chain

def iter_in_slices(iterator, size=None):
    while True:
        slice_iter = islice(iterator, size)
        # If no first object this is how StopIteration is triggered
        peek = next(slice_iter)
        # Put the first object back and return slice
        yield chain([peek], slice_iter)
person Ashley Waite    schedule 02.06.2017
comment
+1: это действительно лучшее решение для огромных n, когда результатом должен быть итератор генераторов. Это требуется в вопросе, и никто, кроме вас, не принял его: если на входе есть генератор, он должен вернуть генератор на выходе!: - person hynekcer; 22.10.2017

Однострочное встроенное решение для этого (поддерживает v2/v3, итераторы, использует стандартную библиотеку и понимание одного генератора):

import itertools
def split_groups(iter_in, group_size):
     return ((x for _, x in item) for _, item in itertools.groupby(enumerate(iter_in), key=lambda x: x[0] // group_size))
person Andrey Cizov    schedule 17.02.2015
comment
Это решение создает бесконечный цикл с последовательностью, например. с диапазоном()/xrange(). - person hynekcer; 22.10.2017

Я думаю, что те вопросы почти равный

Немного изменив, чтобы обрезать последнее, я думаю, что хорошим решением для случая с генератором будет:

from itertools import *
def iter_grouper(n, iterable):
    it = iter(iterable)
    item = itertools.islice(it, n)
    while item:
        yield item
        item = itertools.islice(it, n)

для объекта, который поддерживает срезы (списки, строки, кортежи), мы можем сделать:

def slice_grouper(n, sequence):
   return [sequence[i:i+n] for i in range(0, len(sequence), n)]

теперь это просто вопрос отправки правильного метода:

def grouper(n, iter_or_seq):
    if hasattr(iter_or_seq, "__getslice__"):
        return slice_grouper(n, iter_or_seq)
    elif hasattr(iter_or_seq, "__iter__"):
        return iter_grouper(n, iter_or_seq)

Я думаю, что вы могли бы полировать его немного больше :-)

person fortran    schedule 16.12.2009
comment
Это похоже, и я действительно все еще хочу последний фрагмент. Я просто хочу, чтобы он работал как с генераторами, так и со списками. - person James Brooks; 16.12.2009
comment
о, извините, тогда я неправильно понял эту часть... я исправлю это - person fortran; 16.12.2009
comment
Я думал об этом, но я думал, что должен быть более простой способ, чем hasattr. Роберто Бонвале опубликовал это, чтобы получить ответ. Тем не менее, ваш, кажется, работает +1. - person James Brooks; 16.12.2009
comment
Обратите внимание, что первый пример кода никогда не завершится - person Ryan; 21.07.2021

Почему бы не сделать это так? Выглядит почти как ваша функция splitEvery_2.

def splitEveryN(n, it):
    return [it[i:i+n] for i in range(0, len(it), n)]

На самом деле это только убирает ненужный интервал шагов из среза в вашем решении. :)

person Johannes Charra    schedule 16.12.2009
comment
Именно это я и имел в виду с моей функцией splitEvery_2. Это не работает, если вы вводите выражение генератора. Я думаю, что, вероятно, просто преобразую свой генератор в список, чтобы упростить задачу, но ответ все равно будет беспокоить меня. - person James Brooks; 16.12.2009
comment
Итераторы не поддерживают функцию len, в отличие от списка или кортежа. Например, len(itertools.imap(lambda x:x*2, range(3))) не удастся. - person Cristian Ciupitu; 24.08.2010

Это ответ, который работает как для списка, так и для генератора:

from itertools import count, groupby
def split_every(size, iterable):
    c = count()
    for k, g in groupby(iterable, lambda x: next(c)//size):
        yield list(g) # or yield g if you want to output a generator
person justhalf    schedule 02.05.2014

Вот как вы справляетесь со списком и итератором:

def isList(L): # Implement it somehow - returns True or false
...
return (list, lambda x:x)[int(islist(L))](result)
person Hamish Grubijan    schedule 16.12.2009

это поможет

from itertools import izip_longest
izip_longest(it[::2], it[1::2])

где *it* некоторый итерируемый


Пример:

izip_longest('abcdef'[::2], 'abcdef'[1::2]) -> ('a', 'b'), ('c', 'd'), ('e', 'f')

Давайте сломаем это

'abcdef'[::2] -> 'ace'
'abcdef'[1::2] -> 'bdf'

Как вы можете видеть, последнее число в срезе указывает интервал, который будет использоваться для сбора предметов. Подробнее об использовании расширенных фрагментов можно прочитать здесь.

Функция zip берет первый элемент из первой итерации и объединяет его с первый элемент со вторым итерируемым. Затем функция zip делает то же самое для второго и третьего элементов, пока в одном из итерируемых объектов не закончатся значения.

Результатом является итератор. Если вам нужен список, используйте функцию list() для результата.

person wp-overwatch.com    schedule 20.07.2012
comment
ОП уже знал о почтовом индексе. Но это не работает с генераторами и не включает последний элемент итераций нечетного размера, который, по словам ОП, он хотел. - person DSM; 20.07.2012
comment
Произвольные итерации не поддерживают нарезку (например: xrange(10)[::2] — ошибка). - person Roberto Bonvallet; 09.04.2014

Если вы хотите решение, которое

  • использует только генераторы (без промежуточных списков или кортежей),
  • работает для очень длинных (или бесконечных) итераторов,
  • работает с очень большими партиями,

это делает трюк:

def one_batch(first_value, iterator, batch_size):
    yield first_value
    for i in xrange(1, batch_size):
        yield iterator.next()

def batch_iterator(iterator, batch_size):
    iterator = iter(iterator)
    while True:
        first_value = iterator.next()  # Peek.
        yield one_batch(first_value, iterator, batch_size)

Он работает, просматривая следующее значение в итераторе и передавая его в качестве первого значения генератору (one_batch()), который выдаст его вместе с остальной частью пакета.

Шаг просмотра поднимет StopIteration ровно тогда, когда итератор ввода будет исчерпан и пакетов больше не будет. Поскольку это правильное время для повышения StopIteration в методе batch_iterator(), нет необходимости перехватывать исключение.

Это будет обрабатывать строки из стандартного ввода партиями:

for input_batch in batch_iterator(sys.stdin, 10000):
    for line in input_batch:
        process(line)
    finalise()

Я нашел это полезным для обработки большого количества данных и загрузки результатов во внешнее хранилище.

person Carl    schedule 06.12.2016