(这个问题与this one和this one有关,但这些问题都是在发电机前进行的,这正是我想要避免的事情。
我想将一个发电机拆分成块。要求是:
我尝试过以下代码:
def head(iterable, max=10):
for cnt, el in enumerate(iterable):
yield el
if cnt >= max:
break
def chunks(iterable, size=10):
i = iter(iterable)
while True:
yield head(i, size)
# Sample generator: the real data is much more complex, and expensive to compute
els = xrange(7)
for n, chunk in enumerate(chunks(els, 3)):
for el in chunk:
print 'Chunk %3d, value %d' % (n, el)
这在某种程度上有效:
Chunk 0, value 0
Chunk 0, value 1
Chunk 0, value 2
Chunk 1, value 3
Chunk 1, value 4
Chunk 1, value 5
Chunk 2, value 6
^CTraceback (most recent call last):
File "xxxx.py", line 15, in <module>
for el in chunk:
File "xxxx.py", line 2, in head
for cnt, el in enumerate(iterable):
KeyboardInterrupt
Buuuut ......由于^C
,它永远不会停止(我必须按while True
)。我想在生成器被消耗时停止该循环,但我不知道如何检测这种情况。我试过提出例外:
class NoMoreData(Exception):
pass
def head(iterable, max=10):
for cnt, el in enumerate(iterable):
yield el
if cnt >= max:
break
if cnt == 0 : raise NoMoreData()
def chunks(iterable, size=10):
i = iter(iterable)
while True:
try:
yield head(i, size)
except NoMoreData:
break
# Sample generator: the real data is much more complex, and expensive to compute
els = xrange(7)
for n, chunk in enumerate(chunks(els, 2)):
for el in chunk:
print 'Chunk %3d, value %d' % (n, el)
但是这个例外只是在消费者的背景下提出,这不是我想要的(我想保持消费者代码的清洁)
Chunk 0, value 0
Chunk 0, value 1
Chunk 0, value 2
Chunk 1, value 3
Chunk 1, value 4
Chunk 1, value 5
Chunk 2, value 6
Traceback (most recent call last):
File "xxxx.py", line 22, in <module>
for el in chunk:
File "xxxx.py", line 9, in head
if cnt == 0 : raise NoMoreData
__main__.NoMoreData()
如何在chunks
功能中检测到发电机已耗尽,而无需走路?
答案 0 :(得分:53)
一种方法是查看第一个元素(如果有的话),然后创建并返回实际的生成器。
def head(iterable, max=10):
first = next(iterable) # raise exception when depleted
def head_inner():
yield first # yield the extracted first element
for cnt, el in enumerate(iterable):
yield el
if cnt + 1 >= max: # cnt + 1 to include first
break
return head_inner()
只需在chunk
生成器中使用此功能,然后像处理自定义异常一样捕获StopIteration
异常。
更新:这是另一个版本,使用itertools.islice
替换大部分head
函数和for
循环。事实上,这个简单的for
循环与原始代码中的笨重while-try-next-except-break
构造完全相同,因此结果很多更具可读性
def chunks(iterable, size=10):
iterator = iter(iterable)
for first in iterator: # stops when iterator is depleted
def chunk(): # construct generator for next chunk
yield first # yield element from for loop
for more in islice(iterator, size - 1):
yield more # yield more elements from the iterator
yield chunk() # in outer generator, yield next chunk
我们可以使用itertools.chain
替换内部生成器,甚至更短:
def chunks(iterable, size=10):
iterator = iter(iterable)
for first in iterator:
yield chain([first], islice(iterator, size - 1))
答案 1 :(得分:9)
创建组/块而不是 prewalk 生成器的另一种方法是在使用itertools.groupby
对象的键函数上使用itertools.count
。由于count
对象独立于 iterable ,因此可以轻松生成块,而无需了解 iterable 所拥有的内容。
groupby
的每次迭代都调用next
对象的count
方法并生成一个组/块键(后面是块中的项)根据块的大小对当前计数值进行整数除法。
from itertools import groupby, count
def chunks(iterable, size=10):
c = count()
for _, g in groupby(iterable, lambda _: next(c)//size):
yield g
生成器函数生成的每个group / chunk g
是一个迭代器。但是,由于groupby
对所有组使用共享迭代器,因此组迭代器不能存储在列表或任何容器中,每个组迭代器应在下一个之前使用。
答案 2 :(得分:5)
我能想出最快的解决方案,感谢(在CPython中)使用纯粹的C级内置。通过这样做,不需要Python字节代码来生成每个块(除非底层生成器是用Python实现的),这具有巨大的性能优势。它会在返回之前遍历每个 chunk ,但它不会在它将要返回的块之外进行任何预先行走:
# Py2 only to get generator based map
from future_builtins import map
from itertools import islice, repeat, starmap, takewhile
# operator.truth is *significantly* faster than bool for the case of
# exactly one positional argument
from operator import truth
def chunker(n, iterable): # n is size of each chunk; last chunk may be smaller
return takewhile(truth, map(tuple, starmap(islice, repeat((iter(iterable), n)))))
因为它有点密集,所以展开的版本为插图:
def chunker(n, iterable):
iterable = iter(iterable)
while True:
x = tuple(islice(iterable, n))
if not x:
return
yield x
将chunker
中的enumerate
拨打电话会让您在需要时对这些块进行编号。
答案 3 :(得分:2)
如何使用itertools.islice
:
import itertools
els = iter(xrange(7))
print list(itertools.islice(els, 2))
print list(itertools.islice(els, 2))
print list(itertools.islice(els, 2))
print list(itertools.islice(els, 2))
给出了:
[0, 1]
[2, 3]
[4, 5]
[6]
答案 4 :(得分:2)
from itertools import islice
def chunk(it, n):
'''
# returns chunks of n elements each
>>> list(chunk(range(10), 3))
[
[0, 1, 2, ],
[3, 4, 5, ],
[6, 7, 8, ],
[9, ]
]
>>> list(chunk(list(range(10)), 3))
[
[0, 1, 2, ],
[3, 4, 5, ],
[6, 7, 8, ],
[9, ]
]
'''
def _w(g):
return lambda: tuple(islice(g, n))
return iter(_w(iter(it)), ())
答案 5 :(得分:2)
在制定数据库插入500k +行的更高速度的解决方案时,开始意识到这种情况的有用性。
生成器处理来自源的数据并逐行“生成”它;然后另一个生成器以块的形式对输出进行分组,并按块进行“收益”。第二个生成器只知道块大小,仅此而已。
以下是突出概念的示例:
#!/usr/bin/python
def firstn_gen(n):
num = 0
while num < n:
yield num
num += 1
def chunk_gen(some_gen, chunk_size=7):
res_chunk = []
for count, item in enumerate(some_gen, 1):
res_chunk.append(item)
if count % chunk_size == 0:
yield res_chunk
res_chunk[:] = []
else:
yield res_chunk
if __name__ == '__main__':
for a_chunk in chunk_gen(firstn_gen(33)):
print(a_chunk)
在Python 2.7.12中测试:
[0, 1, 2, 3, 4, 5, 6]
[7, 8, 9, 10, 11, 12, 13]
[14, 15, 16, 17, 18, 19, 20]
[21, 22, 23, 24, 25, 26, 27]
[28, 29, 30, 31, 32]
答案 6 :(得分:1)
我有同样的问题,但找到了比这里提到的更简单的解决方案:
def chunker(iterable, chunk_size):
els = iter(iterable)
while True:
next_el = next(els)
yield chain([next_el], islice(els, chunk_size - 1))
for i, chunk in enumerate(chunker(range(11), 2)):
for el in chunk:
print(i, el)
# Prints the following:
0 0
0 1
1 2
1 3
2 4
2 5
3 6
3 7
4 8
4 9
5 10
答案 7 :(得分:1)
more-itertools提供了chunked和ichunked,它们可以实现目标,Python 3 itertools document page中对此进行了提及。
答案 8 :(得分:0)
你已经说过你不希望将内容存储在内存中,这是否意味着你不能为当前的chunk构建一个中间列表?
为什么不遍历生成器并在块之间插入sentinel值?消费者(或合适的包装者)可以忽略哨兵:
class Sentinel(object):
pass
def chunk(els, size):
for i, el in enumerate(els):
yield el
if i > 0 and i % size == 0:
yield Sentinel
答案 9 :(得分:0)
用发电机发电机编辑其他解决方案
你不应该在你的迭代器中做while True
,而只是遍历它并在每次迭代时更新块号:
def chunk(it, maxv):
n = 0
for i in it:
yield n // mavx, i
n += 1
如果你想要一个发电机发电机,你可以:
def chunk(a, maxv):
def inner(it, maxv, l):
l[0] = False
for i in range(maxv):
yield next(it)
l[0] = True
raise StopIteration
it = iter(a)
l = [True]
while l[0] == True:
yield inner(it, maxv, l)
raise StopIteration
是一个可迭代的。
测试:在python 2.7和3.4上:
for i in chunk(range(7), 3):
print 'CHUNK'
for a in i:
print a
给出:
CHUNK
0
1
2
CHUNK
3
4
5
CHUNK
6
在2.7:
for i in chunk(xrange(7), 3):
print 'CHUNK'
for a in i:
print a
给出相同的结果。
但 BEWARE :list(chunk(range(7))
阻止2.7和3.4
答案 10 :(得分:0)
受Moses Koledoye's answer的启发,我试图提供一种使用itertools.groupby但不需要在每一步进行除法的解决方案。
以下函数可以用作groupby的键,它仅返回一个布尔值,该布尔值在预定义的调用次数后翻转。
def chunks(chunksize=3):
def flag_gen():
flag = False
while True:
for num in range(chunksize):
yield flag
flag = not flag
flag_iter = flag_gen()
def flag_func(*args, **kwargs):
return next(flag_iter)
return flag_func
可以这样使用:
from itertools import groupby
my_long_generator = iter("abcdefghijklmnopqrstuvwxyz")
chunked_generator = groupby(my_long_generator, key=chunks(chunksize=5))
for flag, chunk in chunked_generator:
print("Flag is {f}".format(f=flag), list(chunk))
输出:
Flag is False ['a', 'b', 'c', 'd', 'e']
Flag is True ['f', 'g', 'h', 'i', 'j']
Flag is False ['k', 'l', 'm', 'n', 'o']
Flag is True ['p', 'q', 'r', 's', 't']
Flag is False ['u', 'v', 'w', 'x', 'y']
Flag is True ['z']