как разбить итерацию на куски постоянного размера

86

Возможный дубликат:
как в Python разбить список на части равного размера?

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

Например:

for i in batch(range(0,10), 1): print i
[0]
[1]
...
[9]

или же:

for i in batch(range(0,10), 3): print i
[0,1,2]
[3,4,5]
[6,7,8]
[9]

Теперь я написал то, что считал довольно простым генератором:

def batch(iterable, n = 1):
   current_batch = []
   for item in iterable:
       current_batch.append(item)
       if len(current_batch) == n:
           yield current_batch
           current_batch = []
   if current_batch:
       yield current_batch

Но приведенное выше не дает мне того, чего я ожидал:

for x in   batch(range(0,10),3): print x
[0]
[0, 1]
[0, 1, 2]
[3]
[3, 4]
[3, 4, 5]
[6]
[6, 7]
[6, 7, 8]
[9]

Итак, я что-то упустил, и это, вероятно, свидетельствует о моем полном непонимании генераторов Python. Кто-нибудь захочет указать мне правильное направление?

[Edit: в конце концов я понял, что вышеупомянутое поведение происходит только тогда, когда я запускаю это в ipython, а не в самом python]

Матьё
источник
Хороший вопрос, хорошо написанный, но он уже существует и решит вашу проблему.
Джош Смитон
7
ИМО, это не дубликат. Другой вопрос касается списков, а не итераторов, и для большинства этих ответов требуется len (), что нежелательно для итераторов. Но да, в настоящее время принятый здесь ответ также требует len (), так что ...
dequis
7
Это явно не дубликат. Другие вопросы и ответы работают только для списков , и этот вопрос касается обобщения на все итерации, и это именно тот вопрос, который я имел в виду, когда пришел сюда.
Mark E. Haase
1
@JoshSmeaton @casperOne, это не дубликат, и принятый ответ неверен. Связанный дублирующийся вопрос предназначен для списка, и это для итерации. list предоставляет метод len (), но iterable не предоставляет метод len (), и ответ был бы другим без использования len (). Это правильный ответ: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath
@TrideepRath Ага, я проголосовал за повторное открытие.
Джош Смитон

Ответы:

125

Вероятно, это более эффективно (быстрее)

def batch(iterable, n=1):
    l = len(iterable)
    for ndx in range(0, l, n):
        yield iterable[ndx:min(ndx + n, l)]

for x in batch(range(0, 10), 3):
    print x

Пример использования списка

data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # list of data 

for x in batch(data, 3):
    print(x)

# Output

[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9, 10]

Он избегает создания новых списков.

Карл Ф.
источник
4
Для справки, это самое быстрое решение, которое я нашел: mine = 4.5s, yours = 0.43s, Donkopotamus = 14.8s
mathieu
77
ваша партия фактически принимает список (с len ()), не повторяемый (без len ())
tdihp
31
Это быстрее, потому что это не решение проблемы. Рецепт группировщика Раймонда Хеттингера - в настоящее время ниже - это то, что вы ищете для общего решения, которое не требует, чтобы входной объект имел метод len .
Роберт Мили
7
Почему вы используете min ()? Без min()кода совершенно правильно!
Павел Патрин
21
Итерации нет len(), последовательности естьlen()
Кос
63

FWIW, рецепты в модуле itertools предоставляют этот пример:

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

Это работает так:

>>> list(grouper(3, range(10)))
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]
Раймонд Хеттингер
источник
13
Это не совсем то, что мне нужно, поскольку он дополняет последний элемент набором None. т.е. None - допустимое значение в данных, которые я фактически использую с моей функцией, поэтому вместо этого мне нужно что-то, что не дополняет последнюю запись.
mathieu
12
@mathieu Replace izip_longestwith izip, который не будет дополнять последние записи, а вместо этого обрезает их, когда некоторые элементы начинают заканчиваться.
GoogieK
3
Должен быть zip_longest / zip в python 3
Питер Гердес
5
@GoogieK for x, y in enumerate(grouper(3, xrange(10))): print(x,y)действительно не заполняет значения, он просто полностью удаляет неполный сегмент.
кадрач
3
В качестве одного лайнера , который падает на последний элемент , если неполна: list(zip(*[iter(iterable)] * n)). Это должен быть самый аккуратный фрагмент кода Python, который я когда-либо видел.
Le Frite 02
31

Как отмечали другие, предоставленный вами код делает именно то, что вы хотите. Для другого подхода itertools.isliceвы можете увидеть пример следующего рецепта:

from itertools import islice, chain

def batch(iterable, size):
    sourceiter = iter(iterable)
    while True:
        batchiter = islice(sourceiter, size)
        yield chain([batchiter.next()], batchiter)
донкопотам
источник
1
@abhilash Нет ... этот код использует вызов для того, next()чтобы один StopIterationраз sourceiterбыл исчерпан, тем самым завершая итератор. Без его вызова nextпустые итераторы будут возвращаться бесконечно.
donkopotamus
7
Я должен был заменить batchiter.next()с , next(batchiter)чтобы вышеприведенный код будет работать в Python 3.
Мартин Wiebusch
2
указывая на комментарий из связанной статьи: «Вы должны добавить предупреждение о том, что партия должна быть полностью израсходована, прежде чем вы сможете перейти к следующей». Выход из этого следует употреблять что - то вроде: map(list, batch(xrange(10), 3)). Выполнение: list(batch(xrange(10), 3)приведет к неожиданным результатам.
Nathan Buesgens
2
Не работает на py3. .next()надо поменять на next(..), а list(batch(range(0,10),3))бросаетRuntimeError: generator raised StopIteration
Матье
1
@mathieu: заверните whileцикл в try:/, except StopIteration: returnчтобы исправить последнюю проблему.
ShadowRanger
13

Я дал только один ответ. Однако теперь я считаю, что лучшим решением будет не писать никаких новых функций. More-itertools включает в себя множество дополнительных инструментов и chunkedвходит в их число.

Юнвэй Ву
источник
Это действительно наиболее подходящий ответ (даже если он требует установки еще одного пакета), а также ichunkedдает итерации.
viddik13
10

Странно, похоже, у меня отлично работает в Python 2.x

>>> def batch(iterable, n = 1):
...    current_batch = []
...    for item in iterable:
...        current_batch.append(item)
...        if len(current_batch) == n:
...            yield current_batch
...            current_batch = []
...    if current_batch:
...        yield current_batch
...
>>> for x in batch(range(0, 10), 3):
...     print x
...
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]
Джон Доу
источник
Отличный ответ, потому что ему не нужно ничего импортировать, и он интуитивно понятен.
ojunk
8

Я знаю, что это очень короткий фрагмент кода, который не используется lenи работает как на Python 2, так и на 3 (не мое творение):

def chunks(iterable, size):
    from itertools import chain, islice
    iterator = iter(iterable)
    for first in iterator:
        yield list(chain([first], islice(iterator, size - 1)))
Юнвэй Ву
источник
7

Решение для Python 3.8, если вы работаете с итерациями, которые не определяют lenфункцию и устали:

def batcher(iterable, batch_size):
    while batch := list(islice(iterable, batch_size)):
        yield batch

Пример использования:

def my_gen():
    yield from range(10)
 
for batch in batcher(my_gen(), 3):
    print(batch)

>>> [0, 1, 2]
>>> [3, 4, 5]
>>> [6, 7, 8]
>>> [9]

Конечно, можно реализовать и без оператора моржа.

Атра Адзами
источник
3
В текущей версии batcherпринимает итератор, а не итератор. Например, это приведет к бесконечному циклу со списком. Вероятно, iterator = iter(iterable)перед запуском whileцикла должна быть строка .
Даниэль Перес
2

Это то, что я использую в своем проекте. Он обрабатывает итерации или списки максимально эффективно.

def chunker(iterable, size):
    if not hasattr(iterable, "__len__"):
        # generators don't have len, so fall back to slower
        # method that works with generators
        for chunk in chunker_gen(iterable, size):
            yield chunk
        return

    it = iter(iterable)
    for i in range(0, len(iterable), size):
        yield [k for k in islice(it, size)]


def chunker_gen(generator, size):
    iterator = iter(generator)
    for first in iterator:

        def chunk():
            yield first
            for more in islice(iterator, size - 1):
                yield more

        yield [k for k in chunk()]
Джош Смитон
источник
2
def batch(iterable, n):
    iterable=iter(iterable)
    while True:
        chunk=[]
        for i in range(n):
            try:
                chunk.append(next(iterable))
            except StopIteration:
                yield chunk
                return
        yield chunk

list(batch(range(10), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Атила Ромеро
источник
Лучший ответ на данный момент, работает с любой структурой данных
Клеман Прево
1

Это будет работать для любого итеративного.

from itertools import zip_longest, filterfalse

def batch_iterable(iterable, batch_size=2): 
    args = [iter(iterable)] * batch_size 
    return (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *args))

Это будет работать так:

>>>list(batch_iterable(range(0,5)), 2)
[(0, 1), (2, 3), (4,)]

PS: Это не сработает, если iterable имеет значения None.

Трайдип Рат
источник
1

Вот подход с использованием reduceфункции.

Один лайнер:

from functools import reduce
reduce(lambda cumulator,item: cumulator[-1].append(item) or cumulator if len(cumulator[-1]) < batch_size else cumulator + [[item]], input_array, [[]])

Или более читабельная версия:

from functools import reduce
def batch(input_list, batch_size):
  def reducer(cumulator, item):
    if len(cumulator[-1]) < batch_size:
      cumulator[-1].append(item)
      return cumulator
    else:
      cumulator.append([item])
    return cumulator
  return reduce(reducer, input_list, [[]])

Контрольная работа:

>>> batch([1,2,3,4,5,6,7], 3)
[[1, 2, 3], [4, 5, 6], [7]]
>>> batch(a, 8)
[[1, 2, 3, 4, 5, 6, 7]]
>>> batch([1,2,3,None,4], 3)
[[1, 2, 3], [None, 4]]
Lycha
источник
1

Работоспособная версия без новых функций в python 3.8, адаптированная из ответа @Atra Azami.

import itertools    

def batch_generator(iterable, batch_size=1):
    iterable = iter(iterable)

    while True:
        batch = list(itertools.islice(iterable, batch_size))
        if len(batch) > 0:
            yield batch
        else:
            break

for x in batch_generator(range(0, 10), 3):
    print(x)

Вывод:

[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]
Его Превосходительство
источник
0

Вы можете просто сгруппировать повторяемые элементы по их индексу партии.

def batch(items: Iterable, batch_size: int) -> Iterable[Iterable]:
    # enumerate items and group them by batch index
    enumerated_item_groups = itertools.groupby(enumerate(items), lambda t: t[0] // batch_size)
    # extract items from enumeration tuples
    item_batches = ((t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    return item_batches

Часто бывает так, что вы хотите собрать внутренние итерации, поэтому вот более продвинутая версия.

def batch_advanced(items: Iterable, batch_size: int, batches_mapper: Callable[[Iterable], Any] = None) -> Iterable[Iterable]:
    enumerated_item_groups = itertools.groupby(enumerate(items), lambda t: t[0] // batch_size)
    if batches_mapper:
        item_batches = (batches_mapper(t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    else:
        item_batches = ((t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    return item_batches

Примеры:

print(list(batch_advanced([1, 9, 3, 5, 2, 4, 2], 4, tuple)))
# [(1, 9, 3, 5), (2, 4, 2)]
print(list(batch_advanced([1, 9, 3, 5, 2, 4, 2], 4, list)))
# [[1, 9, 3, 5], [2, 4, 2]]
dimathe47
источник
0

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

def batch(size, i):
    """ Get the i'th batch of the given size """
    return slice(size* i, size* i + size)

Применение:

>>> [1,2,3,4,5,6,7,8,9,10][batch(3, 1)]
>>> [4, 5, 6]

Он получает i-й пакет из последовательности и может работать с другими структурами данных, такими как pandas dataframes ( df.iloc[batch(100,0)]) или numpy array ( array[batch(100,0)]).

Альвитава
источник
0
from itertools import *

class SENTINEL: pass

def batch(iterable, n):
    return (tuple(filterfalse(lambda x: x is SENTINEL, group)) for group in zip_longest(fillvalue=SENTINEL, *[iter(iterable)] * n))

print(list(range(10), 3)))
# outputs: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)]
print(list(batch([None]*10, 3)))
# outputs: [(None, None, None), (None, None, None), (None, None, None), (None,)]
yacc143
источник
0

я использую

def batchify(arr, batch_size):
  num_batches = math.ceil(len(arr) / batch_size)
  return [arr[i*batch_size:(i+1)*batch_size] for i in range(num_batches)]
  
газорпазорп
источник
0

Продолжайте брать (не более) n элементов, пока они не закончатся.

def chop(n, iterable):
    iterator = iter(iterable)
    while chunk := list(take(n, iterator)):
        yield chunk


def take(n, iterable):
    iterator = iter(iterable)
    for i in range(n):
        try:
            yield next(iterator)
        except StopIteration:
            return
В. Чжу
источник