Какой самый «питонный» способ перебрать список по частям? - PullRequest
407 голосов
/ 12 января 2009

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

for i in xrange(0, len(ints), 4):
    # dummy op for example code
    foo += ints[i] * ints[i + 1] + ints[i + 2] * ints[i + 3]

Это похоже на "C-think", что заставляет меня подозревать, что есть более питонический способ справиться с этой ситуацией. Список отбрасывается после итерации, поэтому его не нужно сохранять. Возможно, что-то вроде этого будет лучше?

while ints:
    foo += ints[0] * ints[1] + ints[2] * ints[3]
    ints[0:4] = []

Тем не менее, все еще не совсем "чувствую" себя хорошо. : - /

Смежный вопрос: Как разбить список на куски одинакового размера в Python?

Ответы [ 35 ]

364 голосов
/ 12 января 2009
def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))
# (in python 2 use xrange() instead of range() to avoid allocating a list)

Simple. Легко. Быстро. Работает с любой последовательностью:

text = "I am a very, very helpful text"

for group in chunker(text, 7):
   print repr(group),
# 'I am a ' 'very, v' 'ery hel' 'pful te' 'xt'

print '|'.join(chunker(text, 10))
# I am a ver|y, very he|lpful text

animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish']

for group in chunker(animals, 3):
    print group
# ['cat', 'dog', 'rabbit']
# ['duck', 'bird', 'cow']
# ['gnu', 'fish']
282 голосов
/ 12 января 2009

Изменено из рецептов раздела Python's itertools документов:

from itertools import zip_longest

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

Пример
В псевдокоде, чтобы сохранить пример кратким.

grouper('ABCDEFG', 3, 'x') --> 'ABC' 'DEF' 'Gxx'

Примечание: в Python 2 использовать izip_longest вместо zip_longest.

109 голосов
/ 12 января 2009

Я фанат

chunk_size= 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size
20 голосов
/ 12 января 2009
import itertools
def chunks(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# though this will throw ValueError if the length of ints
# isn't a multiple of four:
for x1,x2,x3,x4 in chunks(ints,4):
    foo += x1 + x2 + x3 + x4

for chunk in chunks(ints,4):
    foo += sum(chunk)

Другой способ:

import itertools
def chunks2(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# x2, x3 and x4 could get the value 0 if the length is not
# a multiple of 4.
for x1,x2,x3,x4 in chunks2(ints,4,0):
    foo += x1 + x2 + x3 + x4
11 голосов
/ 12 января 2009
from itertools import izip_longest

def chunker(iterable, chunksize, filler):
    return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)
9 голосов
/ 15 августа 2013

Мне нужно решение, которое также будет работать с наборами и генераторами. Я не мог придумать ничего очень короткого и красивого, но, по крайней мере, это вполне читабельно.

def chunker(seq, size):
    res = []
    for el in seq:
        res.append(el)
        if len(res) == size:
            yield res
            res = []
    if res:
        yield res

Список:

>>> list(chunker([i for i in range(10)], 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Set:

>>> list(chunker(set([i for i in range(10)]), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Генератор:

>>> list(chunker((i for i in range(10)), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
8 голосов
/ 06 декабря 2012

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

it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9])
for chunk in zip(it, it, it, it):
    print chunk

>>> (1, 2, 3, 4)
>>> (5, 6, 7, 8)

Таким образом, вы не получите последний частичный кусок. Если вы хотите получить (9, None, None, None) в качестве последнего чанка, просто используйте izip_longest из itertools.

7 голосов
/ 29 мая 2012

Идеальное решение этой проблемы работает с итераторами (а не только с последовательностями). Это также должно быть быстро.

Это решение, предоставляемое документацией для itertools:

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return itertools.izip_longest(fillvalue=fillvalue, *args)

Используя ipython's %timeit на моем MacBook Air, я получаю 47,5 нас за цикл.

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

def grouper(size, iterable):
    i = iter(iterable)
    while True:
        out = []
        try:
            for _ in range(size):
                out.append(i.next())
        except StopIteration:
            yield out
            break

        yield out

Простой, но довольно медленный: 693 нас за цикл

Лучшее решение, которое я мог придумать, использует islice для внутреннего цикла:

def grouper(size, iterable):
    it = iter(iterable)
    while True:
        group = tuple(itertools.islice(it, None, size))
        if not group:
            break
        yield group

С тем же набором данных я получаю 305 нас за цикл.

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

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    for i in itertools.izip_longest(fillvalue=fillvalue, *args):
        if tuple(i)[-1] == fillvalue:
            yield tuple(v for v in i if v != fillvalue)
        else:
            yield i

Мне действительно не нравится этот ответ, но он значительно быстрее. 124 доллара США за цикл

5 голосов
/ 12 января 2009

Поскольку никто еще не упомянул об этом, вот решение zip():

>>> def chunker(iterable, chunksize):
...     return zip(*[iter(iterable)]*chunksize)

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

Пример:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9')]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8')]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

Или используя itertools.izip , чтобы вернуть итератор вместо списка:

>>> from itertools import izip
>>> def chunker(iterable, chunksize):
...     return izip(*[iter(iterable)]*chunksize)

Заполнение можно исправить с помощью ответа @ ΤΖΩΤΖΙΟΥ :

>>> from itertools import chain, izip, repeat
>>> def chunker(iterable, chunksize, fillvalue=None):
...     it   = chain(iterable, repeat(fillvalue, chunksize-1))
...     args = [it] * chunksize
...     return izip(*args)
5 голосов
/ 29 ноября 2011

Использование map () вместо zip () исправляет проблему заполнения в ответе Дж. Ф. Себастьяна:

>>> def chunker(iterable, chunksize):
...   return map(None,*[iter(iterable)]*chunksize)

Пример:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9'), ('0', None, None)]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8'), ('9', '0', None, None)]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]
...