リストを均等なサイズのチャンクに分割するにはどうすればよいですか?
私にとって、「均等なサイズのチャンク」とは、それらがすべて同じ長さであるか、そのオプションを除いて、長さの差異が最小限であることを意味します。たとえば、21 個のアイテムに対して 5 つのバスケットを使用すると、次の結果が得られます。
>>> import statistics
>>> statistics.variance([5,5,5,5,1])
3.2
>>> statistics.variance([5,4,4,4,4])
0.19999999999999998
後者の結果を好む実際的な理由: これらの関数を使用して作業を分散していた場合、1 つの関数が他の関数よりも先に終了する可能性が組み込まれているため、他の関数が懸命に作業を続けている間、何もせずに座っていることになります。
他の回答への批判はこちら
私が最初にこの回答を書いたとき、他の回答はどれも均等なサイズのチャンクではありませんでした-それらはすべて最後にラントチャンクを残すため、バランスが取れておらず、長さの分散が必要以上に大きくなっています.
たとえば、現在の上位の回答は次のように終わります。
[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)]
最後に、要素がなくなるまでバスケットのサイクルと一緒に割り当てようとしている要素を zip します。意味的には、これはまさに私たちが望むものです。
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
上記では、アイテムのリストとバスケットの最大数を取得しています。空のリストのリストを作成し、ラウンドロビン スタイルで各要素を追加します。
スライス
別の洗練された解決策は、スライスを使用することです。具体的には、あまり一般的に使用されないスライスへのステップ引数です。すなわち:
start = 0
stop = None
step = number_of_baskets
first_basket = items[start:stop:step]
これは、スライスがデータの長さを気にしないという点で特にエレガントです。結果として、最初のバスケットは必要な長さだけになります。各バスケットの開始点をインクリメントするだけです。
実際、これは 1 行で済む場合もありますが、読みやすくするため、またコードが長くなりすぎるのを避けるために複数行にします。
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]]
contiguous ジェネレーターは、他の 2 つのジェネレーターと同じ長さのパターンでチャンクを提供しますが、項目はすべて順番どおりであり、個別の要素のリストを分割するのと同じくらい均等に分割されていることに注意してください。