Какой самый "питонический” способ перебирать список по частям?
-
10-07-2019 - |
Вопрос
У меня есть скрипт на 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?
Решение
Модифицированный по сравнению с Рецепты раздел Python's итерационные инструменты Документы:
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
.
Другие советы
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']
Я фанат
chunk_size= 4
for i in range(0, len(ints), chunk_size):
chunk = ints[i:i+chunk_size]
# process chunk of size <= chunk_size
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
from itertools import izip_longest
def chunker(iterable, chunksize, filler):
return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)
Мне нужно было решение, которое также работало бы с наборами и генераторами.Я не смог придумать ничего очень короткого и красивого, но, по крайней мере, это вполне читабельно.
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]]
Подобно другим предложениям, но не совсем идентично, мне нравится делать это таким образом, потому что это просто и легко читать:
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
.
Идеальное решение этой проблемы работает с итераторами (а не только с последовательностями).Это также должно быть быстро.
Это решение, предусмотренное документацией для 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
на моем Mac book 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 доллара США за цикл
Поскольку никто еще не упоминал об этом, вот 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)
Заполнение может быть исправлено с помощью @ΤΖΩΤΖΙΟΥ's 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)
Использование map() вместо zip() устраняет проблему с заполнением в J.F.Ответ Себастьяна:
>>> 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')]
Если вы не возражаете против использования внешнего пакета, вы можете использовать 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 Отказ от ответственности: я являюсь автором этого пакета.
Если список большой, самый эффективный способ сделать это - использовать генератор:
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,)
Использование маленьких функций и вещей на самом деле мне не нравится; Я предпочитаю просто использовать ломтики:
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:
...
Другой подход заключается в использовании формы iter
с двумя аргументами:
from itertools import islice
def group(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
Это можно легко адаптировать для использования отступов (это похоже на Markus Jarderot & # 8217; ответ s): р>
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)
С 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 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:]
Однако я не провел измерения производительности, поэтому не знаю, какой из них может быть более эффективным.
Сказав это, я обычно выбираю первый метод. Это не красиво, но это часто является следствием взаимодействия с внешним миром.
Еще один ответ, преимуществами которого являются:
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) break не работает должным образом во внутреннем цикле - управление снова завершится во внутреннем цикле со следующим элементом в итераторе.Чтобы пропустить целые фрагменты, либо оберните внутренний итератор (ii выше) в кортеж, например for c in tuple(ii)
, или установите флаг и исчерпайте итератор.
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
Вы можете использовать раздел или chunks из funcy :
from funcy import partition
for a, b, c, d in partition(4, ints):
foo += a * b * c * d
Эти функции также имеют версии итераторов ipartition
и ichunks
, которые в этом случае будут более эффективными.
Вы также можете посмотреть их реализацию . р>
Чтобы избежать всех преобразований в список 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 в gmail API:
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)
О решении, предоставленном J.F. Sebastian
здесь:
def chunker(iterable, chunksize):
return zip(*[iter(iterable)]*chunksize)
Это умно, но имеет один недостаток - всегда возвращает кортеж.Как получить string вместо этого?
Конечно, вы можете написать ''.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'
Мне нравится этот подход. Он простой и не магический, поддерживает все повторяемые типы и не требует импорта.
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
Я никогда не хочу, чтобы мои куски были дополнены, поэтому это требование является обязательным. Я считаю, что умение работать на любом итерируемом также является требованием. Учитывая это, я решил расширить принятый ответ, https://stackoverflow.com/a/434411/1074659 . Р>
При таком подходе производительность незначительно падает, если заполнение не требуется из-за необходимости сравнивать и фильтровать добавленные значения. Однако для больших размеров блоков эта утилита очень эффективна.
#!/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
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()
Вот чанкер без импорта, который поддерживает генераторы:
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')]
Похоже, что нет красивого способа сделать это. Здесь есть страница с несколькими методами, в том числе:
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
Если списки имеют одинаковый размер, вы можете объединить их в списки из четырех кортежей с помощью 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):
...
Однострочное, временное решение для перебора списка 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 ...
Сначала я разработал его для разделения строк на подстроки для синтаксического анализа строки, содержащей шестнадцатеричное значение.
Сегодня я превратил его в сложный, но все же простой генератор.
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