Какой самый "питонный" способ перебрать список по частям?
У меня есть скрипт 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?
40 ответов
Изменено в разделе рецептов документации iterotools Python:
from itertools import izip_longest
def grouper(iterable, n, fillvalue=None):
args = [iter(iterable)] * n
return izip_longest(*args, fillvalue=fillvalue)
пример
В псевдокоде, чтобы сохранить пример кратко.
grouper('ABCDEFG', 3, 'x') --> 'ABC' 'DEF' 'Gxx'
Замечания: izip_longest
является новым для Python 2.6. В Python 3 используют zip_longest
,
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']
Я фанат
chunkSize= 4
for i in xrange(0, len(ints), chunkSize):
chunk = ints[i:i+chunkSize]
# process chunk of size <= chunkSize
В Python 3.8 вы можете использовать оператор моржа и itertools.islice
.
from itertools import islice
list_ = [i for i in range(10, 100)]
def chunker(it, size):
iterator = iter(it)
while chunk := list(islice(iterator, size)):
print(chunk)
In [2]: chunker(list_, 10)
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69]
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
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
Если вы не возражаете против использования внешнего пакета, вы можете использовать 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 Отказ от ответственности: я автор этого пакета.
В пакете more-itertools есть метод chunked, который делает именно это:
import more_itertools
for s in more_itertools.chunked(range(9), 4):
print(s)
Печать
[0, 1, 2, 3]
[4, 5, 6, 7]
[8]
chunked
возвращает элементы в списке. Если вы предпочитаете итерации, используйте ichunked.
Идеальное решение этой проблемы работает с итераторами (а не только с последовательностями). Это также должно быть быстро.
Это решение, предоставляемое документацией для 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
на моем 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 доллара за петлю
Мне нужно решение, которое также будет работать с наборами и генераторами. Я не мог придумать ничего очень короткого и красивого, но это, по крайней мере, вполне читабельно.
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]]
Задавать:
>>> 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]]
Начиная с Python 3.12, модуль получает функцию , которая специально охватывает итерацию по пакетам входной итерации, где окончательный пакет может быть неполным (каждый пакет представляет собой ). Согласно примеру кода, приведенному в документации:
>>> for batch in batched('ABCDEFG', 3):
... print(batch)
...
('A', 'B', 'C')
('D', 'E', 'F')
('G',)
Примечания к производительности:
Реализация , как и всеitertools
функций на сегодняшний день находится на уровне C, поэтому он способен к оптимизации, код уровня Python не может соответствовать, например
- При каждом извлечении нового пакета он заранее выделяет точно правильный размер (для всех пакетов, кроме последнего), вместо того, чтобы наращивать элемент за элементом с амортизированным ростом, вызывающим множественные перераспределения (то, как решение, вызывающее
islice
делает) - Нужно только поискать
.__next__
функция базового итератора один раз за пакет, а неn
раз за партию (какzip_longest((iter(iterable),) * n)
подход, основанный на подходе, делает) - Проверка в конечном случае — это простой уровень C.
NULL
проверка (тривиальная и в любом случае необходима для обработки возможных исключений) - Обработка конечного случая - это C
goto
с последующим прямымrealloc
(без копирования в меньший размер) вплоть до уже известного окончательного размера, поскольку он отслеживает, сколько элементов он успешно извлек (нет сложного «создать дозорный для использования как и сделать уровень Python»if
/else
проверяет каждую партию, чтобы убедиться, что она пуста, при этом последняя партия требует поиска того, где она появлялась последней, чтобы создать сокращениеtuple
" требуется для решений на основе -).
Учитывая все эти преимущества, оно должно значительно превосходить любое решение уровня Python (даже ), независимо от того, является ли входная итерация длинной или короткой, и независимо от зависит ли размер партии и размер конечной (возможно, неполной) партии ( высокооптимизированное, которое переносит большую часть или всю работу над каждым элементом на уровень Czip_longest
Решения, основанные на использовании гарантированно уникальных значений безопасности, являются наилучшим решением практически для всех случаев, когдаitertools.batched
недоступен, но они могут пострадать в патологических случаях «несколько больших партий, причем последняя партия в основном заполнена не полностью», особенно до версии 3.10, когдаbisect
нельзя использовать для оптимизации отсеченияfillvalue
s изO(n)
линейный поиск доO(log n)
двоичный поиск, ноbatched
полностью избегает этого поиска, поэтому патологические случаи вообще не возникают).
from itertools import izip_longest
def chunker(iterable, chunksize, filler):
return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)
Так как никто еще не упомянул об этом, вот 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)
Подобно другим предложениям, но не совсем идентично, мне нравится делать это таким образом, потому что это просто и легко читается:
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
,
Другой подход заключается в использовании формы с двумя аргументами iter
:
from itertools import islice
def group(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
Это может быть легко адаптировано для использования отступов (это похоже на ответ Маркуса Жардеро):
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)
Отправляя это как ответ, так как я не могу комментировать...
Использование 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')]
Использование маленьких функций и вещей действительно не привлекает меня; Я предпочитаю просто использовать ломтики:
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:
...
Если список большой, самый эффективный способ сделать это - использовать генератор:
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,)
Однострочное, временное решение для перебора списка 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 ...
Чтобы избежать всех преобразований в список 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
так что я (думаю) это задержит разрешение каждого значения, пока оно не будет фактически использовано. К сожалению, ни один из доступных ответов (в настоящее время), казалось, не предлагал этот вариант.
Очевидно, что если вам нужно обрабатывать каждый элемент по очереди, вложите цикл a в 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)
Если я не пропустил что-то, следующее простое решение с выражениями генератора не было упомянуто. Предполагается, что известны как размер, так и количество блоков (что часто имеет место), и что заполнение не требуется:
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))
В вашем втором методе я бы перейти к следующей группе из 4, выполнив это:
ints = ints[4:]
Тем не менее, я не делал никаких измерений производительности, поэтому я не знаю, какой из них может быть более эффективным.
Сказав это, я обычно выбираю первый метод. Это не красиво, но это часто является следствием взаимодействия с внешним миром.
С 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
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(it.next())
except StopIteration:
yield chunk
raise StopIteration
yield chunk
if __name__ == '__main__':
import doctest
doctest.testmod()
Я никогда не хочу, чтобы мои куски были мягкими, поэтому это требование необходимо. Я считаю, что умение работать на любом итерируемом также является требованием. Учитывая это, я решил расширить принятый ответ, /questions/3151787/kakoj-samyij-pitonnyij-sposob-perebrat-spisok-po-chastyam/3151852#3151852.
При таком подходе производительность незначительно падает, если заполнение не требуется из-за необходимости сравнивать и фильтровать дополненные значения. Однако для больших размеров куска эта утилита очень эффективна.
#!/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
вот моя работа над списками, iters и range ... лениво:
def chunker(it,size):
rv = []
for i,el in enumerate(it,1) :
rv.append(el)
if i % size == 0 :
yield rv
rv = []
if rv : yield rv
почти сделал это однострочным; (
In [95]: list(chunker(range(9),2) )
Out[95]: [[0, 1], [2, 3], [4, 5], [6, 7], [8]]
In [96]: list(chunker([1,2,3,4,5],2) )
Out[96]: [[1, 2], [3, 4], [5]]
In [97]: list(chunker(iter(range(9)),2) )
Out[97]: [[0, 1], [2, 3], [4, 5], [6, 7], [8]]
In [98]: list(chunker(range(9),25) )
Out[98]: [[0, 1, 2, 3, 4, 5, 6, 7, 8]]
In [99]: list(chunker(range(9),1) )
Out[99]: [[0], [1], [2], [3], [4], [5], [6], [7], [8]]
In [101]: %timeit list(chunker(range(101),2) )
11.3 µs ± 68.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Мне нравится этот подход. Он прост и не волшебен, поддерживает все повторяемые типы и не требует импорта.
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
Еще один ответ, преимуществами которого являются:
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
Обновить:
Пара недостатков из-за того, что внутренний и внешний циклы извлекают значения из одного и того же итератора:
1) continue не работает должным образом во внешнем цикле - он просто переходит к следующему элементу, а не пропускает фрагмент. Тем не менее, это не кажется проблемой, так как во внешнем цикле проверять нечего.
2) разрыв не работает так, как ожидалось во внутреннем цикле - элемент управления снова окажется во внутреннем цикле со следующим элементом в итераторе. Чтобы пропустить целые куски, либо оберните внутренний итератор (ii выше) в кортеж, например for c in tuple(ii)
или установите флаг и исчерпайте итератор.
Вы можете использовать функцию разбиения или чанков из библиотеки funcy:
from funcy import partition
for a, b, c, d in partition(4, ints):
foo += a * b * c * d
Эти функции также имеют версии итератора ipartition
а также ichunks
, который будет более эффективным в этом случае.
Вы также можете посмотреть на их реализацию.
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
О решении, предоставленном 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'