Простая идиома, чтобы разбить n-длинный список на k-длинные куски, когда n % k > 0?

В Python легко разбить список длиной n на фрагменты размером k, если n кратно k< /em> (IOW, n % k == 0). Вот мой любимый подход (прямо из документы ):

>>> k = 3
>>> n = 5 * k
>>> x = range(k * 5)
>>> zip(*[iter(x)] * k)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14)]

(Хитрость в том, что [iter(x)] * k создает список из k ссылок на тот же самый итератор, что и iter(x). Затем zip генерирует каждый фрагмент, вызывая каждый из k копирует итератор ровно один раз. * перед [iter(x)] * k необходим, потому что zip ожидает получить свои аргументы как "отдельные" итераторы, а не их список.)

Главный недостаток, который я вижу в этой идиоме, заключается в том, что, когда n не кратно k (IOW, n % k > 0), оставшиеся записи просто не учитываются; например.:

>>> zip(*[iter(x)] * (k + 1))
[(0, 1, 2, 3), (4, 5, 6, 7), (8, 9, 10, 11)]

Существует альтернативная идиома, которая немного длиннее для ввода, дает тот же результат, что и приведенная выше, когда n % k == 0, и имеет более приемлемое поведение, когда n % k > 0:

>>> map(None, *[iter(x)] * k)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14)]
>>> map(None, *[iter(x)] * (k + 1))
[(0, 1, 2, 3), (4, 5, 6, 7), (8, 9, 10, 11), (12, 13, 14, None)]

По крайней мере, здесь оставшиеся записи сохраняются, но последний фрагмент дополняется None. Если просто нужно другое значение для заполнения, то itertools.izip_longest решает эта проблема.

Но предположим, что желаемое решение — это решение, в котором последний фрагмент остается незаполненным, т.е.

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

Есть ли простой способ изменить идиому map(None, *[iter(x)]*k) для получения такого результата?

(Конечно, эту проблему несложно решить, написав функцию (см., например, множество прекрасных ответов на Как разбить список на куски одинакового размера? или Какой самый питонический способ перебора списка по частям?). Следовательно, более точным названием для этого вопроса было бы «Как спасти идиому map(None, *[iter(x)]*k)?», но я думаю, что это сбило бы многих читателей с толку.)

Меня поразило, как легко разбить список на куски одинакового размера и как сложно (в сравнении!) избавиться от ненужного заполнения, хотя эти две проблемы кажутся сопоставимая сложность.


person kjo    schedule 10.08.2011    source источник
comment
Вы спрашиваете это по практической причине или просто посмотреть, можно ли это сделать?   -  person Winston Ewert    schedule 10.08.2011
comment
Разве это не дубликат stackoverflow.com/questions/312443/?   -  person Ned Batchelder    schedule 10.08.2011
comment
@Ned Batchelder: я попытался прояснить, что этот пост был его продолжением/расширением (фактически, я цитирую тот же пост stackoverflow в конце). Кроме того, как я пытался объяснить в конце этого поста, этот пост не столько о решении проблемы фрагментации (хорошие решения для нее приведены в постах, которые я цитировал), а скорее о том, чтобы выяснить, есть ли простой способ расширить полезность конкретной идиомы Python. Возможно, сообщениям нужен другой заголовок, но все, что я мог придумать, выглядело запутанным...   -  person kjo    schedule 10.08.2011
comment
Но поскольку мы можем написать функцию для этого, а идиома явно неочевидна, зачем вам это?   -  person Winston Ewert    schedule 10.08.2011


Ответы (3)


sentinal = object()
split = ( 
    (v for v in r if v is not sentinal) for r in
    izip_longest(*[iter(x)]*n, fillvalue=sentinal))

Конечно, лучшая идиома — вызвать функцию, так как это будет более читабельно, чем все, что делает то же самое.

person Winston Ewert    schedule 10.08.2011

из источника IPython:

def chop(seq,size):
    """Chop a sequence into chunks of the given size."""
    chunk = lambda i: seq[i:i+size]
    return map(chunk,xrange(0,len(seq),size))

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

>>> chop(range(12),3)
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]
>>> chop(range(12),4)
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
>>> chop(range(12),5)
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11]]
>>> chop(range(12),6)
[[0, 1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11]]
person sente    schedule 10.08.2011

Как насчет этого? Это другая идиома, но она дает желаемый результат:

[x[i:i+k] for i in range(0,len(x),k)] #=> [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [12, 13, 14]]
[x[i:i+k] for i in range(0,len(x),k)] #=> [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14]]

Или, если вам действительно нужны кортежи, используйте tuple(x[i:i+k]) вместо x[i:i+k].

person jtbandes    schedule 10.08.2011