Понимание генератора Python 3 для генерации фрагментов, включая последние
Если у вас есть список в Python 3.7:
>>> li
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Вы можете превратить это в список кусков каждой длины n
с одной из двух общих идиом Python:
>>> n=3
>>> list(zip(*[iter(li)]*n))
[(0, 1, 2), (3, 4, 5), (6, 7, 8)]
Который капли последнего неполного кортежа, так как (9,10)
не длина n
Вы также можете сделать:
>>> [li[i:i+n] for i in range(0,len(li),n)]
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
если вы хотите получить последний дополнительный список, даже если он содержит менее n
элементов.
Предположим теперь, что у меня есть генератор, gen
, неизвестная длина или прекращение (так что list(gen))
вызовов list(gen))
или sum(1 for _ in gen)
не будет разумным), где я хочу каждый кусок.
Лучшее выражение генератора, которое я смог придумать, - это что-то в этом роде:
from itertools import zip_longest
sentinel=object() # for use in filtering out ending chunks
gen=(e for e in range(22)) # fill in for the actual gen
g3=(t if sentinel not in t else tuple(filter(lambda x: x != sentinel, t)) for t in zip_longest(*[iter(gen)]*n,fillvalue=sentinel))
Это работает по назначению:
>>> next(g3)
(0, 1, 2)
>>> next(g3)
(3, 4, 5)
>>> list(g3)
[(6, 7, 8), (9, 10)]
Это просто кажется - неуклюжий. Я старался:
- с использованием
islice
но отсутствие длины кажется трудно преодолеть; - используя дозорный
iter
в iter
но в дозорной версии iter
требуется вызываемый, а не итерируемый.
Есть ли более идиоматический метод Python 3 для генератора кусков длины n
включая последний патрон, который может быть меньше n
?
Я также открыт для функции генератора. Я ищу что-то идиоматическое и в основном более читаемое.
Обновить:
Метод DSM в его удаленном ответе очень хорош, я думаю:
>>> g3=(iter(lambda it=iter(gen): tuple(islice(it, n)), ()))
>>> next(g3)
(0, 1, 2)
>>> list(g3)
[(3, 4, 5), (6, 7, 8), (9, 10)]
Я открыт для того, чтобы этот вопрос был дублированным, но связанный вопрос почти 10 лет и сосредоточен на списке. Нет нового метода в Python 3 с генераторами, где вы не знаете длину и не хотите больше, чем кусок за раз?
Ответы
Ответ 1
Я думаю, что это всегда будет беспорядочно, пока вы пытаетесь поместить это в один лайнер. Я бы просто укусил пулю и пошел с генераторной функцией. Особенно полезно, если вы не знаете фактический размер (скажем, если gen
- бесконечный генератор и т.д.).
from itertools import islice
def chunk(gen, k):
"""Efficiently split 'gen' into chunks of size 'k'.
Args:
gen: Iterator to chunk.
k: Number of elements per chunk.
Yields:
Chunks as a list.
"""
while True:
chunk = [*islice(gen, 0, k)]
if chunk:
yield chunk
else:
break
>>> gen = iter(list(range(11)))
>>> list(chunk(gen))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
У кого-то может быть лучшее предложение, но я так и сделаю.
Ответ 2
Это похоже на довольно разумный подход, который строится именно на itertools.
>>> g = (i for i in range(10))
>>> g3 = takewhile(lambda x: x, (list(islice(g,3)) for _ in count(0)))
>>> list(g3)
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Ответ 3
Я собрал несколько таймингов для ответов здесь.
Способ, который я изначально написал, на самом деле является самым быстрым на Python 3.7. Для одного лайнера это, скорее всего, самое лучшее.
Модифицированная версия ответа на холодную скорость быстро и Pythonic и читается.
Другие ответы - все схожие скорости.
Контрольный показатель:
from __future__ import print_function
try:
from itertools import zip_longest, takewhile, islice, count
except ImportError:
from itertools import takewhile, islice, count
from itertools import izip_longest as zip_longest
from collections import deque
def f1(it,k):
sentinel=object()
for t in (t if sentinel not in t else tuple(filter(lambda x: x != sentinel, t)) for t in zip_longest(*[iter(it)]*k, fillvalue=sentinel)):
yield t
def f2(it,k):
for t in (iter(lambda it=iter(it): tuple(islice(it, k)), ())):
yield t
def f3(it,k):
while True:
chunk = (*islice(it, 0, k),) # tuple(islice(it, 0, k)) if Python < 3.5
if chunk:
yield chunk
else:
break
def f4(it,k):
for t in takewhile(lambda x: x, (tuple(islice(it,k)) for _ in count(0))):
yield t
if __name__=='__main__':
import timeit
def tf(f, k, x):
data=(y for y in range(x))
return deque(f(data, k), maxlen=3)
k=3
for f in (f1,f2,f3,f4):
print(f.__name__, tf(f,k,100000))
for case, x in (('small',10000),('med',100000),('large',1000000)):
print("Case {}, {:,} x {}".format(case,x,k))
for f in (f1,f2,f3,f4):
print(" {:^10s}{:.4f} secs".format(f.__name__, timeit.timeit("tf(f, k, x)", setup="from __main__ import f, tf, x, k", number=10)))
И результаты:
f1 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
f2 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
f3 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
f4 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
Case small, 10,000 x 3
f1 0.0125 secs
f2 0.0231 secs
f3 0.0185 secs
f4 0.0250 secs
Case med, 100,000 x 3
f1 0.1239 secs
f2 0.2270 secs
f3 0.1845 secs
f4 0.2477 secs
Case large, 1,000,000 x 3
f1 1.2140 secs
f2 2.2431 secs
f3 1.7967 secs
f4 2.4697 secs
Ответ 4
Это решение с функцией генератора довольно явное и короткое:
def g3(seq):
it = iter(seq)
while True:
head = list(itertools.islice(it, 3))
if head:
yield head
else:
break
Ответ 5
В itertools recipe
различные помощники генератора.
Здесь вы можете изменить take
со второй формой iter
для создания генератора блоков.
from itertools import islice
def chunks(n, it):
it = iter(it)
return iter(lambda: tuple(islice(it, n)), ())
пример
li = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(*chunks(3, li))
Выход
(0, 1, 2) (3, 4, 5) (6, 7, 8) (9, 10)
Ответ 6
more_itertools.chunked
:
list(more_itertools.chunked(range(11), 3))
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
См. Также источник:
iter(functools.partial(more_itertools.take, n, iter(iterable)), [])
Ответ 7
Моя попытка с помощью groupby
и cycle
. С cycle
вы можете выбрать шаблон, как группировать элементы, поэтому он универсален:
from itertools import groupby, cycle
gen=(e for e in range(11))
d = [list(g) for d, g in groupby(gen, key=lambda v, c=cycle('000111'): next(c))]
print([v for v in d])
Выходы:
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
Ответ 8
мы можем сделать это, используя функцию grouper, указанную на странице документации itertools.
from itertools import zip_longest
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
args = [iter(iterable)] * n
return zip_longest(fillvalue=fillvalue, *args)
def out_iterator(lst):
for each in grouper(lst,n):
if None in each:
yield each[:each.index(None)]
else:
yield each
a=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
n=3
print(list(out_iterator(a)))
Выход:
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10)]