Каков самый "питонический" способ перебора списка в кусках?

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

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?

Ответы

Ответ 1

Модифицированный из рецептов секции Python 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.

Ответ 2

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)

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

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']

Ответ 3

Я фанат

chunk_size= 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size

Ответ 4

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

Ответ 5

from itertools import izip_longest

def chunker(iterable, chunksize, filler):
    return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)

Ответ 6

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

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]]

Ответ 7

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

Это решение, предоставленное документацией для 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 %timeit на моем mac book air, я получаю 47,5 us за цикл.

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

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 us за цикл

Ответ 8

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

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.

Ответ 9

Поскольку никто еще не упомянул об этом решение 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)

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

>>> 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)

Ответ 10

Использование map() вместо zip() решает проблему заполнения в ответе JF Sebastian:

>>> 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')]

Ответ 11

Если вы не против использования внешнего пакета, вы можете использовать iteration_utilities.grouper из iteration_utilties 1. Он поддерживает все итерации (не только последовательности):

from iteration_utilities import grouper
seq = list(range(20))
for group in grouper(seq, 4):
    print(group)

который печатает:

(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9, 10, 11)
(12, 13, 14, 15)
(16, 17, 18, 19)

Если длина не кратная группе, она также поддерживает заполнение (неполную последнюю группу) или усечение (отбрасывание неполной последней группы) последней:

from iteration_utilities import grouper
seq = list(range(17))
for group in grouper(seq, 4):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16,)

for group in grouper(seq, 4, fillvalue=None):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16, None, None, None)

for group in grouper(seq, 4, truncate=True):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)

1 Отказ от ответственности: я являюсь автором этого пакета.

Ответ 12

Если список большой, самым эффективным способом сделать это будет использование генератора:

def get_chunk(iterable, chunk_size):
    result = []
    for item in iterable:
        result.append(item)
        if len(result) == chunk_size:
            yield tuple(result)
            result = []
    if len(result) > 0:
        yield tuple(result)

for x in get_chunk([1,2,3,4,5,6,7,8,9,10], 3):
    print x

(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10,)

Ответ 13

Использование небольших функций и вещей на самом деле не привлекает меня; Я предпочитаю использовать только срезы:

data = [...]
chunk_size = 10000 # or whatever
chunks = [data[i:i+chunk_size] for i in xrange(0,len(data),chunk_size)]
for chunk in chunks:
    ...

Ответ 14

Другим подходом было бы использование двухфакторной формы iter:

from itertools import islice

def group(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

Это можно легко адаптировать для использования отступов (это похоже на ответ Markus Jarderot):

from itertools import islice, chain, repeat

def group_pad(it, size, pad=None):
    it = chain(iter(it), repeat(pad))
    return iter(lambda: tuple(islice(it, size)), (pad,) * size)

Они могут быть объединены для дополнительного заполнения:

_no_pad = object()
def group(it, size, pad=_no_pad):
    if pad == _no_pad:
        it = iter(it)
        sentinel = ()
    else:
        it = chain(iter(it), repeat(pad))
        sentinel = (pad,) * size
    return iter(lambda: tuple(islice(it, size)), sentinel)

Ответ 15

С NumPy это просто:

ints = array([1, 2, 3, 4, 5, 6, 7, 8])
for int1, int2 in ints.reshape(-1, 2):
    print(int1, int2)

выход:

1 2
3 4
5 6
7 8

Ответ 16

Если я не пропустил что-то, следующее простое решение с выражениями генератора не было упомянуто. Предполагается, что известны как размер, так и количество блоков (что часто имеет место), и что заполнение не требуется:

def chunks(it, n, m):
    """Make an iterator over m first chunks of size n.
    """
    it = iter(it)
    # Chunks are presented as tuples.
    return (tuple(next(it) for _ in range(n)) for _ in range(m))

Ответ 17

В вашем втором методе я перейду к следующей группе из 4, выполнив следующее:

ints = ints[4:]

Однако я не выполнил никаких измерений производительности, поэтому я не знаю, какая из них может быть более эффективной.

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

Ответ 18

Еще один ответ, преимущества которого:

1) Легко понятный
2) Работает над любыми итерабельными, а не только последовательностями (некоторые из вышеперечисленных ответов будут заглушать файловые дескрипторы)
3) Не загружает кусок в память сразу - 4) Не делает длинный список ссылок на один и тот же итератор в памяти

5) Отсутствие заполнения значений заполнения в конце списка

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

def chunkiter(iterable, size):
  def inneriter(first, iterator, size):
    yield first
    for _ in xrange(size - 1): 
      yield iterator.next()
  it = iter(iterable)
  while True:
    yield inneriter(it.next(), it, size)

In [2]: i = chunkiter('abcdefgh', 3)
In [3]: for ii in i:                                                
          for c in ii:
            print c,
          print ''
        ...:     
        a b c 
        d e f 
        g h 

Update:
Несколько недостатков из-за того, что внутренняя и внешняя петли вытягивают значения из одного и того же итератора:
1) продолжение не работает должным образом во внешнем цикле - оно просто переходит к следующему элементу, а не пропускает кусок. Однако это не похоже на проблему, так как в внешнем цикле нечего тестировать.
2) прерывание не работает должным образом во внутреннем цикле - управление снова завершится во внутреннем цикле со следующим элементом в итераторе. Чтобы пропустить целые куски, оберните внутренний итератор (ii выше) в кортеж, например. for c in tuple(ii), или установите флаг и выпустите итератор.

Ответ 19

def group_by(iterable, size):
    """Group an iterable into lists that don't exceed the size given.

    >>> group_by([1,2,3,4,5], 2)
    [[1, 2], [3, 4], [5]]

    """
    sublist = []

    for index, item in enumerate(iterable):
        if index > 0 and index % size == 0:
            yield sublist
            sublist = []

        sublist.append(item)

    if sublist:
        yield sublist

Ответ 20

Вы можете использовать partition или chunks из funcy библиотека:

from funcy import partition

for a, b, c, d in partition(4, ints):
    foo += a * b * c * d

Эти функции также имеют версии итератора ipartition и ichunks, которые в этом случае будут более эффективными.

Вы также можете заглянуть в их реализацию.

Ответ 21

Чтобы избежать всех преобразований в список import itertools и:

>>> for k, g in itertools.groupby(xrange(35), lambda x: x/10):
...     list(g)

Выдает:

... 
0 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
2 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3 [30, 31, 32, 33, 34]
>>> 

Я проверил groupby, и он не конвертируется в список или использует len, поэтому я (думаю) это задержит разрешение каждого значения до тех пор, пока оно не будет использовано. К сожалению, ни один из доступных ответов (в это время), казалось, не предлагал этого варианта.

Очевидно, что если вам нужно обработать каждый элемент, в свою очередь, запустите цикл for через g:

for k,g in itertools.groupby(xrange(35), lambda x: x/10):
    for i in g:
       # do what you need to do with individual items
    # now do what you need to do with the whole group

Моя особая заинтересованность в этом заключалась в необходимости использовать генератор для отправки изменений в партии до 1000 в API gmail:

    messages = a_generator_which_would_not_be_smart_as_a_list
    for idx, batch in groupby(messages, lambda x: x/1000):
        batch_request = BatchHttpRequest()
        for message in batch:
            batch_request.add(self.service.users().messages().modify(userId='me', id=message['id'], body=msg_labels))
        http = httplib2.Http()
        self.credentials.authorize(http)
        batch_request.execute(http=http)

Ответ 22

О решении, предоставленном J.F. Sebastian здесь:

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

Это умный, но имеет один недостаток - всегда возвращать кортеж. Как получить строку вместо?
Конечно, вы можете написать ''.join(chunker(...)), но временный кортеж построен так или иначе.

Вы можете избавиться от временного кортежа, написав собственный zip, например:

class IteratorExhausted(Exception):
    pass

def translate_StopIteration(iterable, to=IteratorExhausted):
    for i in iterable:
        yield i
    raise to # StopIteration would get ignored because this is generator,
             # but custom exception can leave the generator.

def custom_zip(*iterables, reductor=tuple):
    iterators = tuple(map(translate_StopIteration, iterables))
    while True:
        try:
            yield reductor(next(i) for i in iterators)
        except IteratorExhausted: # when any of iterators get exhausted.
            break

Тогда

def chunker(data, size, reductor=tuple):
    return custom_zip(*[iter(data)]*size, reductor=reductor)

Пример использования:

>>> for i in chunker('12345', 2):
...     print(repr(i))
...
('1', '2')
('3', '4')
>>> for i in chunker('12345', 2, ''.join):
...     print(repr(i))
...
'12'
'34'

Ответ 23

Вот чанкер без импорта, который поддерживает генераторы:

def chunks(seq, size):
    it = iter(seq)
    while True:
        ret = tuple(next(it) for _ in range(size))
        if len(ret) == size:
            yield ret
        else:
            raise StopIteration()

Пример использования:

>>> def foo():
...     i = 0
...     while True:
...         i += 1
...         yield i
...
>>> c = chunks(foo(), 3)
>>> c.next()
(1, 2, 3)
>>> c.next()
(4, 5, 6)
>>> list(chunks('abcdefg', 2))
[('a', 'b'), ('c', 'd'), ('e', 'f')]

Ответ 24

def chunker(iterable, n):
    """Yield iterable in chunk sizes.

    >>> chunks = chunker('ABCDEF', n=4)
    >>> chunks.next()
    ['A', 'B', 'C', 'D']
    >>> chunks.next()
    ['E', 'F']
    """
    it = iter(iterable)
    while True:
        chunk = []
        for i in range(n):
            try:
                chunk.append(next(it))
            except StopIteration:
                yield chunk
                raise StopIteration
        yield chunk

if __name__ == '__main__':
    import doctest

    doctest.testmod()

Ответ 25

Мне нравится этот подход. Он кажется простым и не волшебным и поддерживает все повторяющиеся типы и не требует импорта.

def chunk_iter(iterable, chunk_size):
it = iter(iterable)
while True:
    chunk = tuple(next(it) for _ in range(chunk_size))
    if not chunk:
        break
    yield chunk

Ответ 26

Я никогда не хочу, чтобы мои куски дополнялись, так что это необходимо. Я считаю, что способность работать с любым итерабельным также является требованием. Учитывая это, я решил продолжить принятый ответ fooobar.com/questions/3041/....

Производительность имеет небольшой успех в этом подходе, если заполнение не требуется из-за необходимости сравнивать и фильтровать заполненные значения. Однако для больших размеров блоков эта утилита очень эффективна.

#!/usr/bin/env python3
from itertools import zip_longest


_UNDEFINED = object()


def chunker(iterable, chunksize, fillvalue=_UNDEFINED):
    """
    Collect data into chunks and optionally pad it.

    Performance worsens as `chunksize` approaches 1.

    Inspired by:
        https://docs.python.org/3/library/itertools.html#itertools-recipes

    """
    args = [iter(iterable)] * chunksize
    chunks = zip_longest(*args, fillvalue=fillvalue)
    yield from (
        filter(lambda val: val is not _UNDEFINED, chunk)
        if chunk[-1] is _UNDEFINED
        else chunk
        for chunk in chunks
    ) if fillvalue is _UNDEFINED else chunks

Ответ 27

Кажется, что это не очень хороший способ сделать это. Здесь - это страница с несколькими способами, включая:

def split_seq(seq, size):
    newseq = []
    splitsize = 1.0/size*len(seq)
    for i in range(size):
        newseq.append(seq[int(round(i*splitsize)):int(round((i+1)*splitsize))])
    return newseq

Ответ 28

Если списки имеют одинаковый размер, вы можете объединить их в списки из 4-х кортежей с zip(). Например:

# Four lists of four elements each.

l1 = range(0, 4)
l2 = range(4, 8)
l3 = range(8, 12)
l4 = range(12, 16)

for i1, i2, i3, i4 in zip(l1, l2, l3, l4):
    ...

Здесь, что создает функция zip():

>>> print l1
[0, 1, 2, 3]
>>> print l2
[4, 5, 6, 7]
>>> print l3
[8, 9, 10, 11]
>>> print l4
[12, 13, 14, 15]
>>> print zip(l1, l2, l3, l4)
[(0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)]

Если списки большие, и вы не хотите их комбинировать в более крупный список, используйте itertools.izip(), который создает итератор, а не список.

from itertools import izip

for i1, i2, i3, i4 in izip(l1, l2, l3, l4):
    ...

Ответ 29

Однострочное решение adhoc для перебора списка x в кусках размера 4 -

for a, b, c, d in zip(x[0::4], x[1::4], x[2::4], x[3::4]):
    ... do something with a, b, c and d ...

Ответ 30

Сначала я сконструировал его для разбиения строк на подстроки для синтаксического анализа строки, содержащей hex.
Сегодня я превратил его в сложный, но все же простой генератор.

def chunker(iterable, size, reductor, condition):
    it = iter(iterable)
    def chunk_generator():
        return (next(it) for _ in range(size))
    chunk = reductor(chunk_generator())
    while condition(chunk):
        yield chunk
        chunk = reductor(chunk_generator())

Аргументы:

Очевидные

  • iterable - любой итеративный/итератор/генератор, контактирующий/генерирующий/итерирующий по входным данным,
  • size - это, конечно, размер куска, который вы хотите получить,

Более интересно

  • reductor является вызываемым, который получает генератор, итерирующий содержимое содержимого. Я ожидаю, что он вернет последовательность или строку, но я не требую этого.

    Вы можете передать этот аргумент, например, list, tuple, set, frozenset,
    или что-то интересное. Я передал бы эту функцию, возвращая строку
    (при условии, что iterable содержит/генерирует/итерации по строкам):

    def concatenate(iterable):
        return ''.join(iterable)
    

    Обратите внимание, что reductor может вызвать замыкающий генератор, создавая исключение.

  • condition является вызываемым, который получает все, что возвращается reductor.
    Он решает утвердить и уступить его (возвращая что-либо, оценивая на True),
    или отказаться от него и закончить работу генератора (возвращая что-то другое или поднимая исключение).

    Когда количество элементов в iterable не делится на size, когда it исчерпывается, reductor получит генератор, генерирующий меньше элементов, чем size.
    Позвольте называть эти элементы последними.

    Я пригласил две функции для передачи в качестве этого аргумента:

    • lambda x:x - будут получены последние элементы.

    • lambda x: len(x)==<size> - последние элементы будут отклонены.
      замените <size> на число, равное size