Как разделить список на части одинакового размера?
«Куски одинакового размера» для меня означает, что все они имеют одинаковую длину или, за исключением этого варианта, с минимальным различием в длине. Например, 5 корзин по 21 предмету могут дать следующие результаты:
>>> import statistics
>>> statistics.variance([5,5,5,5,1])
3.2
>>> statistics.variance([5,4,4,4,4])
0.19999999999999998
Практическая причина предпочесть последний результат: если вы использовали эти функции для распределения работы, у вас была заложена перспектива того, что одна из них, вероятно, закончит намного раньше других, поэтому она будет сидеть и ничего не делать, в то время как другие будут продолжать усердно работать.
Критика других ответов здесь
Когда я изначально писал этот ответ, ни один из других ответов не был кусками одинакового размера - все они оставляют короткий кусок в конце, поэтому они плохо сбалансированы и имеют более высокое, чем необходимо, изменение длины.
Например, текущий лучший ответ заканчивается на:
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
[70, 71, 72, 73, 74]]
Другие, как list(grouper(3, range(7)))
, и chunk(range(7), 3)
как возвращение: [(0, 1, 2), (3, 4, 5), (6, None, None)]
. Это None
просто набивка и, на мой взгляд, довольно неэлегантная. Они НЕ равномерно разбивают итерируемые объекты.
Почему мы не можем разделить их лучше?
Цикл Решение
Использование сбалансированного решения высокого уровня itertools.cycle
, как я мог бы это сделать сегодня. Вот установка:
from itertools import cycle
items = range(10, 75)
number_of_baskets = 10
Теперь нам нужны наши списки для заполнения элементов:
baskets = [[] for _ in range(number_of_baskets)]
Наконец, мы заархивируем элементы, которые собираемся выделить, вместе с циклом корзин, пока у нас не закончатся элементы, что семантически это именно то, что мы хотим:
for element, basket in zip(items, cycle(baskets)):
basket.append(element)
Вот результат:
>>> from pprint import pprint
>>> pprint(baskets)
[[10, 20, 30, 40, 50, 60, 70],
[11, 21, 31, 41, 51, 61, 71],
[12, 22, 32, 42, 52, 62, 72],
[13, 23, 33, 43, 53, 63, 73],
[14, 24, 34, 44, 54, 64, 74],
[15, 25, 35, 45, 55, 65],
[16, 26, 36, 46, 56, 66],
[17, 27, 37, 47, 57, 67],
[18, 28, 38, 48, 58, 68],
[19, 29, 39, 49, 59, 69]]
Чтобы реализовать это решение, мы пишем функцию и предоставляем аннотации типов:
from itertools import cycle
from typing import List, Any
def cycle_baskets(items: List[Any], maxbaskets: int) -> List[List[Any]]:
baskets = [[] for _ in range(min(maxbaskets, len(items)))]
for item, basket in zip(items, cycle(baskets)):
basket.append(item)
return baskets
В приведенном выше примере мы берем наш список предметов и максимальное количество корзин. Мы создаем список пустых списков, в который добавляем каждый элемент в циклическом стиле.
Ломтики
Еще одно элегантное решение - использовать срезы - в частности, менее часто используемый аргумент step для срезов. то есть:
start = 0
stop = None
step = number_of_baskets
first_basket = items[start:stop:step]
Это особенно элегантно, поскольку срезы не заботятся о длине данных - результат, наша первая корзина, имеет ровно столько, сколько нужно. Нам нужно только увеличить начальную точку для каждой корзины.
Фактически, это может быть однострочный текст, но мы сделаем его многострочным для удобства чтения и во избежание чрезмерно длинной строки кода:
from typing import List, Any
def slice_baskets(items: List[Any], maxbaskets: int) -> List[List[Any]]:
n_baskets = min(maxbaskets, len(items))
return [items[i::n_baskets] for i in range(n_baskets)]
А islice
модуль itertools предоставит ленивый итерационный подход, подобный тому, о котором изначально просили в вопросе.
Я не ожидаю, что большинство вариантов использования принесут большую пользу, поскольку исходные данные уже полностью материализованы в списке, но для больших наборов данных это может сэкономить почти половину использования памяти.
from itertools import islice
from typing import List, Any, Generator
def yield_islice_baskets(items: List[Any], maxbaskets: int) -> Generator[List[Any], None, None]:
n_baskets = min(maxbaskets, len(items))
for i in range(n_baskets):
yield islice(items, i, None, n_baskets)
Просматривайте результаты с помощью:
from pprint import pprint
items = list(range(10, 75))
pprint(cycle_baskets(items, 10))
pprint(slice_baskets(items, 10))
pprint([list(s) for s in yield_islice_baskets(items, 10)])
Обновленные предыдущие решения
Вот еще одно сбалансированное решение, адаптированное из функции, которую я использовал в производстве в прошлом, которая использует оператор по модулю:
def baskets_from(items, maxbaskets=25):
baskets = [[] for _ in range(maxbaskets)]
for i, item in enumerate(items):
baskets[i % maxbaskets].append(item)
return filter(None, baskets)
И я создал генератор, который делает то же самое, если вы поместите его в список:
def iter_baskets_from(items, maxbaskets=3):
'''generates evenly balanced baskets from indexable iterable'''
item_count = len(items)
baskets = min(item_count, maxbaskets)
for x_i in range(baskets):
yield [items[y_i] for y_i in range(x_i, item_count, baskets)]
И наконец, поскольку я вижу, что все вышеперечисленные функции возвращают элементы в непрерывном порядке (в том виде, в котором они были заданы):
def iter_baskets_contiguous(items, maxbaskets=3, item_count=None):
'''
generates balanced baskets from iterable, contiguous contents
provide item_count if providing a iterator that doesn't support len()
'''
item_count = item_count or len(items)
baskets = min(item_count, maxbaskets)
items = iter(items)
floor = item_count // baskets
ceiling = floor + 1
stepdown = item_count % baskets
for x_i in range(baskets):
length = ceiling if x_i < stepdown else floor
yield [items.next() for _ in range(length)]
Выход
Чтобы проверить их:
print(baskets_from(range(6), 8))
print(list(iter_baskets_from(range(6), 8)))
print(list(iter_baskets_contiguous(range(6), 8)))
print(baskets_from(range(22), 8))
print(list(iter_baskets_from(range(22), 8)))
print(list(iter_baskets_contiguous(range(22), 8)))
print(baskets_from('ABCDEFG', 3))
print(list(iter_baskets_from('ABCDEFG', 3)))
print(list(iter_baskets_contiguous('ABCDEFG', 3)))
print(baskets_from(range(26), 5))
print(list(iter_baskets_from(range(26), 5)))
print(list(iter_baskets_contiguous(range(26), 5)))
Что распечатывает:
[[0], [1], [2], [3], [4], [5]]
[[0], [1], [2], [3], [4], [5]]
[[0], [1], [2], [3], [4], [5]]
[[0, 8, 16], [1, 9, 17], [2, 10, 18], [3, 11, 19], [4, 12, 20], [5, 13, 21], [6, 14], [7, 15]]
[[0, 8, 16], [1, 9, 17], [2, 10, 18], [3, 11, 19], [4, 12, 20], [5, 13, 21], [6, 14], [7, 15]]
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [12, 13, 14], [15, 16, 17], [18, 19], [20, 21]]
[['A', 'D', 'G'], ['B', 'E'], ['C', 'F']]
[['A', 'D', 'G'], ['B', 'E'], ['C', 'F']]
[['A', 'B', 'C'], ['D', 'E'], ['F', 'G']]
[[0, 5, 10, 15, 20, 25], [1, 6, 11, 16, 21], [2, 7, 12, 17, 22], [3, 8, 13, 18, 23], [4, 9, 14, 19, 24]]
[[0, 5, 10, 15, 20, 25], [1, 6, 11, 16, 21], [2, 7, 12, 17, 22], [3, 8, 13, 18, 23], [4, 9, 14, 19, 24]]
[[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]]
Обратите внимание, что непрерывный генератор предоставляет фрагменты той же длины, что и два других, но все элементы в порядке, и они разделены так же поровну, как можно разделить список дискретных элементов.