як розділити ітерабель на шматки постійного розміру


85

Можливий дублікат: Як розділити список на однакові за
розміром фрагменти в Python?

Я здивований, що не зміг знайти функцію "batch", яка брала б як вхідний файл ітерабель і повертала ітерабель ітерацій.

Наприклад:

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. Хтось хотів би спрямувати мене в правильному напрямку?

[Редагувати: врешті-решт я зрозумів, що вищевказана поведінка відбувається лише тоді, коли я запускаю це в ipython, а не в самому python]


Гарне запитання, добре написане, але воно вже існує і вирішить вашу проблему.
Джош Смітон

7
ІМО це насправді не дублікат. Інше питання зосереджується на списках замість ітераторів, і більшість з цих відповідей вимагає len (), що є небажаним для ітераторів. Але е, для прийнятої нині відповіді тут також потрібно len (), отже ...
dequis

7
Це явно не дублікат. Інші запитання та відповіді працюють лише для списків , і це питання стосується узагальнення для всіх ітерацій, саме це питання я мав на увазі, коли прийшов сюди.
Mark E. Haase

1
@JoshSmeaton @casperOne це не дублікат, і прийнята відповідь є неправильною. Пов’язане повторюване запитання призначене для списку, а це для ітеративного. list надає метод len (), але ітерабельний не надає метод len (), і відповідь буде іншою без використання len () Це правильна відповідь: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath

@TrideepRath так, я проголосував за відкриття.
Джош Смітон

Відповіді:


119

Це, мабуть, ефективніше (швидше)

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
Для протоколу, це найшвидше рішення, яке я знайшов: моє = 4,5 с, твоє = 0,43 с, Донкопотам = 14,8 с
Матьє

74
ваша партія насправді приймає список (з len ()), не ітерабельний (без len ())
tdihp

28
Це швидше, оскільки це не вирішення проблеми. Рецепт окуня Реймонда Хеттінгера - на даний момент нижче цього - це те, що ви шукаєте для загального рішення, яке не вимагає, щоб об'єкт введення мав метод len .
Robert E Mealey,

7
Чому ви використовуєте min ()? Без min()коду цілком правильно!
Павло Патрін

20
Сумісні речі не мають len(), послідовності маютьlen()
Кос

60

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 Замініть izip_longestна izip, який не заповнить останні записи, а замість цього обріже записи, коли деякі елементи почнуть закінчуватися.
GoogieK

3
Має бути zip_longest / zip у python 3
Пітер Гердес,

5
@GoogieK for x, y in enumerate(grouper(3, xrange(10))): print(x,y)дійсно не заповнює значення, він просто взагалі скидає неповний сегмент.
kadrach

3
В якості одного лайнера , який падає на останній елемент , якщо неповна: list(zip(*[iter(iterable)] * n)). Це повинен бути найшвидший фрагмент коду python, який я коли-небудь бачив.
Le

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нього продовжуватиметься повертати порожні ітератори на невизначений час.
донкопотамус,

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
mathieu

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

4

Рішення для 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]

Звичайно, це може бути реалізовано і без оператора моржів.


1
У поточній версії 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: Це не буде працювати, якщо ітерація має значення None.


1

Ось підхід із використанням reduceфункції.

Oneliner:

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

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

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

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.