Простая идиома для разбиения n-длинного списка на k-длинные порции, когда n% k> 0? - PullRequest
12 голосов
/ 10 августа 2011

В Python легко разбить длинный список n на куски k , если n кратен k (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), оставленные записи просто пропускаются ; e.g.:

>>> 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)?», Но я думаю, что это сбило бы с толку много читателей.)

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

Ответы [ 4 ]

15 голосов
/ 10 августа 2011
[x[i:i+k] for i in range(0,n,k)]
3 голосов
/ 10 августа 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]]
3 голосов
/ 10 августа 2011
sentinal = object()
split = ( 
    (v for v in r if v is not sentinal) for r in
    izip_longest(*[iter(x)]*n, fillvalue=sentinal))

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

1 голос
/ 10 августа 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].

...