Как разбить список на куски одинакового размера? - PullRequest
1920 голосов
/ 23 ноября 2008

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

Мне было интересно, есть ли у кого-нибудь хорошее решение для списков любой длины, например, используя генераторы.

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

Смежный вопрос: Какой самый «питонный» способ перебрать список по частям?

Ответы [ 58 ]

3 голосов
/ 11 января 2017

У меня есть одно решение, которое работает, но более важным, чем это решение, является несколько комментариев о других подходах. Во-первых, хорошее решение не должно требовать, чтобы один цикл проходил через итераторы по порядку. Если я бегу

g = paged_iter(list(range(50)), 11))
i0 = next(g)
i1 = next(g)
list(i1)
list(i0)

Соответствующий вывод для последней команды:

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

не

 []

Поскольку большинство решений на основе itertools возвращаются сюда. Это не просто обычное скучное ограничение доступа к итераторам по порядку. Представьте себе, что потребитель пытается очистить плохо введенные данные, которые поменяли местами соответствующий порядок блоков, равный 5, т.е. данные выглядят как [B5, A5, D5, C5] и должны выглядеть как [A5, B5, C5, D5] (где А5 это всего лишь пять элементов, а не подсписок). Этот потребитель посмотрел бы на заявленное поведение функции группировки и без колебаний написал бы цикл вроде

i = 0
out = []
for it in paged_iter(data,5)
    if (i % 2 == 0):
         swapped = it
    else: 
         out += list(it)
         out += list(swapped)
    i = i + 1

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

Во-вторых, приличное число предлагаемых решений неявно зависит от того факта, что итераторы имеют детерминированный порядок (например, они не установлены), и хотя некоторые решения, использующие islice, могут быть в порядке, меня это беспокоит.

В-третьих, подход группирования itertools работает, но рецепт опирается на внутреннее поведение функций zip_longest (или zip), которое не является частью их опубликованного поведения. В частности, функция группирования работает только потому, что в zip_longest (i0 ... in) следующая функция всегда вызывается в следующем порядке (i0), next (i1), ... next (in) перед началом заново. Когда группер передает n копий одного и того же объекта итератора, он полагается на это поведение.

Наконец, хотя решение, представленное ниже, может быть улучшено, если вы критикуете вышеизложенное предположение о том, что к итераторам обращаются по порядку и что они полностью используются без этого предположения, ДОЛЖНЫ неявно (через цепочку вызовов) или явно (через запросы или другую структуру данных). ) хранить элементы для каждого подтератора где-нибудь. Так что не тратьте время (как я), полагая, что можно обойти это с помощью некоторого умного трюка.

def paged_iter(iterat, n):
    itr = iter(iterat)
    deq = None
    try:
        while(True):
            deq = collections.deque(maxlen=n)
            for q in range(n):
                deq.append(next(itr))
            yield (i for i in deq)
    except StopIteration:
        yield (i for i in deq)
3 голосов
/ 03 ноября 2017

Не думаю, что видел эту опцию, поэтому просто добавлю еще одну :)):

def chunks(iterable, chunk_size):
  i = 0;
  while i < len(iterable):
    yield iterable[i:i+chunk_size]
    i += chunk_size
3 голосов
/ 08 марта 2017

Вот идея использования itertools.groupby:

def chunks(l, n):
    c = itertools.count()
    return (it for _, it in itertools.groupby(l, lambda x: next(c)//n))

Возвращает генератор генераторов. Если вы хотите список списков, просто замените последнюю строку на

    return [list(it) for _, it in itertools.groupby(l, lambda x: next(c)//n)]

Пример возврата списка списков:

>>> chunks('abcdefghij', 4)
[['a', 'b', 'c', 'd'], ['e', 'f', 'g', 'h'], ['i', 'j']]

(Так что да, это страдает от "проблемы с пустяками", которая может или не может быть проблемой в данной ситуации.)

2 голосов
/ 03 сентября 2014

Как @AaronHall, я попал сюда в поисках кусков примерно одинакового размера. Есть разные интерпретации этого. В моем случае, если желаемый размер N, я бы хотел, чтобы каждая группа имела размер> = N. Таким образом, сироты, созданные в большинстве вышеперечисленных, должны быть перераспределены на другие группы.

Это можно сделать с помощью:

def nChunks(l, n):
    """ Yield n successive chunks from l.
    Works for lists,  pandas dataframes, etc
    """
    newn = int(1.0 * len(l) / n + 0.5)
    for i in xrange(0, n-1):
        yield l[i*newn:i*newn+newn]
    yield l[n*newn-newn:]

(из Разделение списка на N частей приблизительно равной длины ), просто называя его nChunks (l, l / n) или nChunks (l, floor (l / n)) *

2 голосов
/ 30 октября 2017

Не волшебство, но просто и правильно:

def chunks(iterable, n):
    """Yield successive n-sized chunks from iterable."""
    values = []
    for i, item in enumerate(iterable, 1):
        values.append(item)
        if i % n == 0:
            yield values
            values = []
    if values:
        yield values
2 голосов
/ 17 октября 2015

Так как я должен был сделать что-то вроде этого, вот мое решение с учетом генератора и размера партии:

def pop_n_elems_from_generator(g, n):
    elems = []
    try:
        for idx in xrange(0, n):
            elems.append(g.next())
        return elems
    except StopIteration:
        return elems
2 голосов
/ 18 сентября 2015

Я пришел к следующему решению без создания объекта временного списка, который должен работать с любым повторяемым объектом. Обратите внимание, что эта версия для Python 2.x:

def chunked(iterable, size):
    stop = []
    it = iter(iterable)
    def _next_chunk():
        try:
            for _ in xrange(size):
                yield next(it)
        except StopIteration:
            stop.append(True)
            return

    while not stop:
        yield _next_chunk()

for it in chunked(xrange(16), 4):
   print list(it)

Выход:

[0, 1, 2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11]
[12, 13, 14, 15] 
[]

Как вы видите, если len (итерируемый)% size == 0, тогда у нас есть дополнительный пустой объект итератора. Но я не думаю, что это большая проблема.

2 голосов
/ 13 сентября 2013
  • Работает с любой итерацией
  • Внутренние данные - объект генератора (не список)
  • Один вкладыш
In [259]: get_in_chunks = lambda itr,n: ( (v for _,v in g) for _,g in itertools.groupby(enumerate(itr),lambda (ind,_): ind/n))

In [260]: list(list(x) for x in get_in_chunks(range(30),7))
Out[260]:
[[0, 1, 2, 3, 4, 5, 6],
 [7, 8, 9, 10, 11, 12, 13],
 [14, 15, 16, 17, 18, 19, 20],
 [21, 22, 23, 24, 25, 26, 27],
 [28, 29]]
1 голос
/ 23 марта 2018

Мне не нравится идея разделения элементов по размеру куска, например, Скрипт может разделить от 101 до 3 блоков на [50, 50, 1]. Для моих нужд мне нужно было делиться пропорционально и поддерживать порядок. Сначала я написал свой собственный скрипт, который отлично работает, и он очень прост. Но позже я увидел этот ответ , где сценарий лучше моего, я рекомендую его. Вот мой сценарий:

def proportional_dividing(N, n):
    """
    N - length of array (bigger number)
    n - number of chunks (smaller number)
    output - arr, containing N numbers, diveded roundly to n chunks
    """
    arr = []
    if N == 0:
        return arr
    elif n == 0:
        arr.append(N)
        return arr
    r = N // n
    for i in range(n-1):
        arr.append(r)
    arr.append(N-r*(n-1))

    last_n = arr[-1]
    # last number always will be r <= last_n < 2*r
    # when last_n == r it's ok, but when last_n > r ...
    if last_n > r:
        # ... and if difference too big (bigger than 1), then
        if abs(r-last_n) > 1:
            #[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 7] # N=29, n=12
            # we need to give unnecessary numbers to first elements back
            diff = last_n - r
            for k in range(diff):
                arr[k] += 1
            arr[-1] = r
            # and we receive [3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2]
    return arr

def split_items(items, chunks):
    arr = proportional_dividing(len(items), chunks)
    splitted = []
    for chunk_size in arr:
        splitted.append(items[:chunk_size])
        items = items[chunk_size:]
    print(splitted)
    return splitted

items = [1,2,3,4,5,6,7,8,9,10,11]
chunks = 3
split_items(items, chunks)
split_items(['a','b','c','d','e','f','g','h','i','g','k','l', 'm'], 3)
split_items(['a','b','c','d','e','f','g','h','i','g','k','l', 'm', 'n'], 3)
split_items(range(100), 4)
split_items(range(99), 4)
split_items(range(101), 4)

и вывод:

[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]
[['a', 'b', 'c', 'd'], ['e', 'f', 'g', 'h'], ['i', 'g', 'k', 'l', 'm']]
[['a', 'b', 'c', 'd', 'e'], ['f', 'g', 'h', 'i', 'g'], ['k', 'l', 'm', 'n']]
[range(0, 25), range(25, 50), range(50, 75), range(75, 100)]
[range(0, 25), range(25, 50), range(50, 75), range(75, 99)]
[range(0, 25), range(25, 50), range(50, 75), range(75, 101)]
1 голос
/ 07 июля 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))
...